fix: align pi rust p1 official features
This commit is contained in:
@@ -28,6 +28,7 @@
|
||||
START: '/api/page-ai/pi/start',
|
||||
SEND: '/api/page-ai/pi/send',
|
||||
ABORT: '/api/page-ai/pi/abort',
|
||||
RPC_COMMAND: '/api/page-ai/pi/rpc-command',
|
||||
UI_RESPONSE: '/api/page-ai/pi/ui-response',
|
||||
EVENTS: '/api/page-ai/pi/events',
|
||||
BOOTSTRAP: '/api/page-ai/pi/bootstrap',
|
||||
@@ -97,6 +98,7 @@
|
||||
modelOptions: [],
|
||||
history: [],
|
||||
pendingQueue: { steering: [], followUp: [] },
|
||||
pendingImages: [],
|
||||
viewingHistorySessionId: null,
|
||||
contextSelection: {
|
||||
currentPage: false,
|
||||
@@ -1647,6 +1649,10 @@
|
||||
window.location.assign('/user/ai#ai-admin-access');
|
||||
return;
|
||||
}
|
||||
if (action === 'attach-file' || action === 'camera') {
|
||||
pickPiPromptImages(action === 'camera');
|
||||
return;
|
||||
}
|
||||
if (action === 'send-steer') {
|
||||
sendCurrentInput({ streamingBehavior: 'steer' });
|
||||
return;
|
||||
@@ -1655,6 +1661,14 @@
|
||||
sendCurrentInput({ streamingBehavior: 'followUp' });
|
||||
return;
|
||||
}
|
||||
if (action === 'abort-bash') {
|
||||
callPiRpcCommand('abort_bash', {}, 5000).then(function () {
|
||||
showPiToast('已请求中止 bash', 'info');
|
||||
}).catch(function (error) {
|
||||
showPiToast(error && error.message ? error.message : 'bash 中止失败', 'warning');
|
||||
});
|
||||
return;
|
||||
}
|
||||
if (action === 'plan-mode') {
|
||||
var input = piLabPanelEl && piLabPanelEl.querySelector('[data-page-ai-pi-lab-input]');
|
||||
var current = input ? input.value.trim() : '';
|
||||
@@ -1943,12 +1957,13 @@
|
||||
'<div class="wolai-page-ai-pi-lab-menu-sep"></div>' +
|
||||
'<div class="wolai-page-ai-pi-lab-menu-section">权限与附件</div>' +
|
||||
'<button type="button" class="wolai-page-ai-pi-lab-menu-item" data-page-ai-pi-lab-menu-action="directory-permission">' + piIcon('folder') + '<span>目录权限</span></button>' +
|
||||
'<button type="button" class="wolai-page-ai-pi-lab-menu-item" data-page-ai-pi-lab-menu-action="attach-file" disabled title="Pi RPC 附件上下文尚未接入">' + piIcon('image') + '<span>添加图片和文件</span></button>' +
|
||||
'<button type="button" class="wolai-page-ai-pi-lab-menu-item" data-page-ai-pi-lab-menu-action="camera" disabled title="Pi RPC 附件上下文尚未接入">' + piIcon('camera') + '<span>拍照</span></button>' +
|
||||
'<button type="button" class="wolai-page-ai-pi-lab-menu-item" data-page-ai-pi-lab-menu-action="attach-file">' + piIcon('image') + '<span>添加图片和文件</span></button>' +
|
||||
'<button type="button" class="wolai-page-ai-pi-lab-menu-item" data-page-ai-pi-lab-menu-action="camera">' + piIcon('camera') + '<span>拍照</span></button>' +
|
||||
'<div class="wolai-page-ai-pi-lab-menu-sep"></div>' +
|
||||
'<div class="wolai-page-ai-pi-lab-menu-section">执行中发送</div>' +
|
||||
'<button type="button" class="wolai-page-ai-pi-lab-menu-item" data-page-ai-pi-lab-menu-action="send-steer">' + piIcon('zap') + '<span>作为引导发送</span></button>' +
|
||||
'<button type="button" class="wolai-page-ai-pi-lab-menu-item" data-page-ai-pi-lab-menu-action="send-followup">' + piIcon('queue') + '<span>作为排队追问发送</span></button>' +
|
||||
'<button type="button" class="wolai-page-ai-pi-lab-menu-item" data-page-ai-pi-lab-menu-action="abort-bash">' + piIcon('stop') + '<span>中止 bash</span></button>' +
|
||||
'<button type="button" class="wolai-page-ai-pi-lab-menu-item" data-page-ai-pi-lab-menu-action="plan-mode" title="通过 Pi Rust 官方 plan-mode 扩展进入计划评审">' + piIcon('settings') + '<span>计划评审</span></button>' +
|
||||
'</div>' +
|
||||
'<div class="wolai-page-ai-pi-lab-modebar" data-page-ai-pi-lab-modebar>' +
|
||||
@@ -3386,6 +3401,71 @@
|
||||
}).catch(function () {});
|
||||
}
|
||||
|
||||
function callPiRpcCommand(type, params, timeoutMs) {
|
||||
if (!piLabState.sessionId) return Promise.reject(new Error('Pi 会话尚未就绪'));
|
||||
return fetch(API.RPC_COMMAND, {
|
||||
method: 'POST',
|
||||
credentials: 'same-origin',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
sessionId: piLabState.sessionId,
|
||||
type: type,
|
||||
params: params || {},
|
||||
timeoutMs: timeoutMs || 10000,
|
||||
}),
|
||||
}).then(function (r) {
|
||||
return responseJsonOrError(r, 'Pi RPC command failed');
|
||||
});
|
||||
}
|
||||
|
||||
function readFileAsPiImage(file) {
|
||||
return new Promise(function (resolve, reject) {
|
||||
if (!file || !/^image\//i.test(file.type || '')) {
|
||||
reject(new Error('只支持图片附件;普通文件请在输入中使用 @文件路径'));
|
||||
return;
|
||||
}
|
||||
var reader = new FileReader();
|
||||
reader.onload = function () {
|
||||
var dataUrl = String(reader.result || '');
|
||||
var comma = dataUrl.indexOf(',');
|
||||
if (comma < 0) {
|
||||
reject(new Error('图片读取失败'));
|
||||
return;
|
||||
}
|
||||
resolve({
|
||||
type: 'image',
|
||||
source: {
|
||||
type: 'base64',
|
||||
mediaType: file.type || 'image/png',
|
||||
data: dataUrl.slice(comma + 1),
|
||||
},
|
||||
});
|
||||
};
|
||||
reader.onerror = function () { reject(new Error('图片读取失败')); };
|
||||
reader.readAsDataURL(file);
|
||||
});
|
||||
}
|
||||
|
||||
function pickPiPromptImages(useCamera) {
|
||||
var input = document.createElement('input');
|
||||
input.type = 'file';
|
||||
input.accept = 'image/*';
|
||||
input.multiple = !useCamera;
|
||||
if (useCamera) input.setAttribute('capture', 'environment');
|
||||
input.addEventListener('change', function () {
|
||||
var files = Array.prototype.slice.call(input.files || []);
|
||||
if (!files.length) return;
|
||||
Promise.all(files.map(readFileAsPiImage)).then(function (images) {
|
||||
piLabState.pendingImages = (piLabState.pendingImages || []).concat(images);
|
||||
showPiToast('已添加 ' + images.length + ' 张图片', 'success');
|
||||
updateButtons();
|
||||
}).catch(function (error) {
|
||||
showPiToast(error && error.message ? error.message : '附件读取失败', 'warning');
|
||||
});
|
||||
});
|
||||
input.click();
|
||||
}
|
||||
|
||||
|
||||
function responseJsonOrError(response, fallbackMessage) {
|
||||
return response.json().catch(function () { return {}; }).then(function (payload) {
|
||||
@@ -3947,6 +4027,10 @@
|
||||
selectedContext: contextSelection,
|
||||
};
|
||||
if (streamingBehavior) body.streamingBehavior = streamingBehavior;
|
||||
if (piLabState.pendingImages && piLabState.pendingImages.length) {
|
||||
body.images = piLabState.pendingImages;
|
||||
piLabState.pendingImages = [];
|
||||
}
|
||||
return fetch(API.SEND, {
|
||||
method: 'POST',
|
||||
credentials: 'same-origin',
|
||||
|
||||
@@ -1905,7 +1905,10 @@ pub(crate) fn load_effective_ai_runtime_policy(
|
||||
.into_iter()
|
||||
.filter_map(|entry| normalize_model_ref(Some(&default_provider), &entry.id))
|
||||
.collect::<Vec<_>>();
|
||||
if !allowed_models.iter().any(|model| model == FREEFIRST_PI_MODEL) {
|
||||
if !allowed_models
|
||||
.iter()
|
||||
.any(|model| model == FREEFIRST_PI_MODEL)
|
||||
{
|
||||
allowed_models.push(FREEFIRST_PI_MODEL.to_string());
|
||||
}
|
||||
if !allowed_models.iter().any(|model| model == DEFAULT_PI_MODEL) {
|
||||
|
||||
@@ -604,6 +604,7 @@ pub fn build_router(state: AppState) -> Router {
|
||||
.route("/api/page-ai/pi/configure", post(page_ai_pi::configure))
|
||||
.route("/api/page-ai/pi/state", post(page_ai_pi::state))
|
||||
.route("/api/page-ai/pi/compact", post(page_ai_pi::compact))
|
||||
.route("/api/page-ai/pi/rpc-command", post(page_ai_pi::rpc_command))
|
||||
.route(
|
||||
"/api/page-ai/pi/queue-config",
|
||||
post(page_ai_pi::queue_config),
|
||||
|
||||
@@ -43,6 +43,7 @@ const PI_LAB_SCHEMA_EVENT: &str = "mnote.page_ai_pi.event.v1";
|
||||
const PI_LAB_SCHEMA_STATE: &str = "mnote.page_ai_pi.state.v1";
|
||||
const PI_LAB_SCHEMA_COMPACT: &str = "mnote.page_ai_pi.compact.v1";
|
||||
const PI_LAB_SCHEMA_QUEUE_CONFIG: &str = "mnote.page_ai_pi.queue_config.v1";
|
||||
const PI_LAB_SCHEMA_RPC_COMMAND: &str = "mnote.page_ai_pi.rpc_command.v1";
|
||||
const PI_LAB_DEFAULT_MODEL_PROVIDER: &str = "omniroute";
|
||||
const PI_LAB_SCHEMA_SESSION_TREE: &str = "mnote.page_ai_pi.session_tree.v1";
|
||||
const PI_LAB_SCHEMA_FORK: &str = "mnote.page_ai_pi.fork.v1";
|
||||
@@ -263,6 +264,7 @@ pub struct PiLabSendRequest {
|
||||
pub session_id: String,
|
||||
pub message: String,
|
||||
pub streaming_behavior: Option<String>,
|
||||
pub images: Option<Vec<Value>>,
|
||||
pub root_uri: Option<String>,
|
||||
pub workspace_id: Option<String>,
|
||||
pub page_path: Option<String>,
|
||||
@@ -278,6 +280,17 @@ pub struct PiLabAbortRequest {
|
||||
pub session_id: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct PiLabRpcCommandRequest {
|
||||
pub session_id: String,
|
||||
#[serde(rename = "type", alias = "command")]
|
||||
pub command_type: String,
|
||||
#[serde(default)]
|
||||
pub params: Value,
|
||||
pub timeout_ms: Option<u64>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct PiLabRenameSessionRequest {
|
||||
@@ -723,10 +736,7 @@ fn resolve_file_path(
|
||||
.filter(|value| !value.is_empty());
|
||||
let relative_path = if let Some(folder_path) = folder_path {
|
||||
let normalized_path = path.replace('\\', "/").trim_start_matches('/').to_string();
|
||||
let normalized_folder = folder_path
|
||||
.replace('\\', "/")
|
||||
.trim_matches('/')
|
||||
.to_string();
|
||||
let normalized_folder = folder_path.replace('\\', "/").trim_matches('/').to_string();
|
||||
if normalized_folder.is_empty()
|
||||
|| Path::new(&normalized_path).is_absolute()
|
||||
|| normalized_path == normalized_folder
|
||||
@@ -2568,7 +2578,10 @@ fn stage_extension_file(
|
||||
Ok(target_path.to_string_lossy().to_string())
|
||||
}
|
||||
|
||||
fn bind_mnote_bridge_context_path(extension_path: &str, context_path: &Path) -> Result<(), WebError> {
|
||||
fn bind_mnote_bridge_context_path(
|
||||
extension_path: &str,
|
||||
context_path: &Path,
|
||||
) -> Result<(), WebError> {
|
||||
let target = Path::new(extension_path);
|
||||
let source = fs::read_to_string(target).map_err(|error| {
|
||||
WebError::internal(format!(
|
||||
@@ -2579,18 +2592,23 @@ fn bind_mnote_bridge_context_path(extension_path: &str, context_path: &Path) ->
|
||||
let canonical_context_path = context_path
|
||||
.canonicalize()
|
||||
.unwrap_or_else(|_| context_path.to_path_buf());
|
||||
let context_literal = serde_json::to_string(&canonical_context_path.to_string_lossy().to_string())
|
||||
.map_err(|error| WebError::internal(format!("编码 Pi Rust MNote context 路径失败: {error}")))?;
|
||||
let context_payload: Value = serde_json::from_slice(&fs::read(context_path).map_err(|error| {
|
||||
WebError::internal(format!(
|
||||
"读取 Pi Rust MNote context 失败: {}: {error}",
|
||||
context_path.display()
|
||||
))
|
||||
})?)
|
||||
.map_err(|error| WebError::internal(format!("解析 Pi Rust MNote context 失败: {error}")))?;
|
||||
let context_snapshot_literal = serde_json::to_string(&context_payload)
|
||||
.map_err(|error| WebError::internal(format!("编码 Pi Rust MNote context 快照失败: {error}")))?;
|
||||
let context_file_needle = r#"const DEFAULT_CONTEXT_FILE = path.join(EXTENSION_DIR, "mnote-context.json");"#;
|
||||
let context_literal = serde_json::to_string(
|
||||
&canonical_context_path.to_string_lossy().to_string(),
|
||||
)
|
||||
.map_err(|error| WebError::internal(format!("编码 Pi Rust MNote context 路径失败: {error}")))?;
|
||||
let context_payload: Value =
|
||||
serde_json::from_slice(&fs::read(context_path).map_err(|error| {
|
||||
WebError::internal(format!(
|
||||
"读取 Pi Rust MNote context 失败: {}: {error}",
|
||||
context_path.display()
|
||||
))
|
||||
})?)
|
||||
.map_err(|error| WebError::internal(format!("解析 Pi Rust MNote context 失败: {error}")))?;
|
||||
let context_snapshot_literal = serde_json::to_string(&context_payload).map_err(|error| {
|
||||
WebError::internal(format!("编码 Pi Rust MNote context 快照失败: {error}"))
|
||||
})?;
|
||||
let context_file_needle =
|
||||
r#"const DEFAULT_CONTEXT_FILE = path.join(EXTENSION_DIR, "mnote-context.json");"#;
|
||||
let context_snapshot_needle =
|
||||
r#"const EMBEDDED_CONTEXT: Record<string, unknown> | undefined = undefined;"#;
|
||||
if !source.contains(context_file_needle) || !source.contains(context_snapshot_needle) {
|
||||
@@ -3218,8 +3236,6 @@ async fn start_runtime_for_session(
|
||||
.arg(&mnote_pi_extension_path)
|
||||
.arg("--tools")
|
||||
.arg(pi_tool_names.join(","))
|
||||
.arg("--no-prompt-templates")
|
||||
.arg("--no-context-files")
|
||||
.stdin(Stdio::piped())
|
||||
.stdout(Stdio::piped())
|
||||
.stderr(Stdio::piped());
|
||||
@@ -3675,6 +3691,66 @@ fn rpc_response_error_message(response: Option<&Value>, fallback: &str) -> Strin
|
||||
.to_string()
|
||||
}
|
||||
|
||||
fn normalize_pi_rpc_command_type(value: &str) -> String {
|
||||
match value.trim() {
|
||||
"follow-up" | "followUp" | "queue-follow-up" | "queueFollowUp" => "follow_up",
|
||||
"get-state" | "getState" => "get_state",
|
||||
"get-messages" | "getMessages" => "get_messages",
|
||||
"get-available-models" | "getAvailableModels" => "get_available_models",
|
||||
"set-model" | "setModel" => "set_model",
|
||||
"set-thinking-level" | "setThinkingLevel" => "set_thinking_level",
|
||||
"set-steering-mode" | "setSteeringMode" => "set_steering_mode",
|
||||
"set-follow-up-mode" | "setFollowUpMode" => "set_follow_up_mode",
|
||||
"set-auto-compaction" | "setAutoCompaction" => "set_auto_compaction",
|
||||
"new-session" | "newSession" => "new_session",
|
||||
"switch-session" | "switchSession" => "switch_session",
|
||||
"set-session-name" | "setSessionName" => "set_session_name",
|
||||
"export-html" | "exportHtml" => "export_html",
|
||||
"abort-bash" | "abortBash" => "abort_bash",
|
||||
other => other,
|
||||
}
|
||||
.to_string()
|
||||
}
|
||||
|
||||
fn pi_lab_allowed_rpc_commands() -> HashSet<&'static str> {
|
||||
[
|
||||
"get_state",
|
||||
"get_messages",
|
||||
"get_available_models",
|
||||
"new_session",
|
||||
"switch_session",
|
||||
"set_session_name",
|
||||
"export_html",
|
||||
"abort_bash",
|
||||
"cycle_model",
|
||||
]
|
||||
.into_iter()
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn build_pi_rpc_command(command_type: &str, params: Value) -> Result<Value, WebError> {
|
||||
let command_type = normalize_pi_rpc_command_type(command_type);
|
||||
if !pi_lab_allowed_rpc_commands().contains(command_type.as_str()) {
|
||||
return Err(WebError::bad_request_code(
|
||||
"page_ai_pi_lab_rpc_command_not_allowed",
|
||||
format!("Pi RPC command 不允许通过 MNote wrapper 调用: {command_type}"),
|
||||
));
|
||||
}
|
||||
let mut command = match params {
|
||||
Value::Object(map) => Value::Object(map),
|
||||
Value::Null => json!({}),
|
||||
_ => {
|
||||
return Err(WebError::bad_request_code(
|
||||
"page_ai_pi_lab_rpc_command_params_invalid",
|
||||
"Pi RPC command params 必须是 object",
|
||||
));
|
||||
}
|
||||
};
|
||||
command["id"] = json!(generate_id("pi_rpc_command"));
|
||||
command["type"] = json!(command_type);
|
||||
Ok(command)
|
||||
}
|
||||
|
||||
fn apply_text_operations(current: &str, operations: &Value) -> Result<String, WebError> {
|
||||
match operations {
|
||||
Value::Array(ops) => {
|
||||
@@ -4149,8 +4225,13 @@ impl PiLabToolFacade {
|
||||
}
|
||||
|
||||
fn local_file_read(&self, params: Value) -> Result<Value, WebError> {
|
||||
let (target, root_uri, relative_path) =
|
||||
resolve_file_path(&self.state, &self.context, ¶ms, self.session.as_ref(), false)?;
|
||||
let (target, root_uri, relative_path) = resolve_file_path(
|
||||
&self.state,
|
||||
&self.context,
|
||||
¶ms,
|
||||
self.session.as_ref(),
|
||||
false,
|
||||
)?;
|
||||
let content = fs::read_to_string(&target).map_err(|error| {
|
||||
WebError::bad_request_code(
|
||||
"page_ai_pi_lab_file_read_failed",
|
||||
@@ -4168,8 +4249,13 @@ impl PiLabToolFacade {
|
||||
}
|
||||
|
||||
fn local_file_patch(&self, params: Value) -> Result<Value, WebError> {
|
||||
let (target, root_uri, relative_path) =
|
||||
resolve_file_path(&self.state, &self.context, ¶ms, self.session.as_ref(), true)?;
|
||||
let (target, root_uri, relative_path) = resolve_file_path(
|
||||
&self.state,
|
||||
&self.context,
|
||||
¶ms,
|
||||
self.session.as_ref(),
|
||||
true,
|
||||
)?;
|
||||
let before_version = file_version(&target);
|
||||
let current = fs::read_to_string(&target).unwrap_or_default();
|
||||
let next = if let Some(content) = params.get("content").and_then(Value::as_str) {
|
||||
@@ -5045,6 +5131,7 @@ pub async fn bootstrap(
|
||||
.to_string(),
|
||||
message: prompt.to_string(),
|
||||
streaming_behavior: None,
|
||||
images: None,
|
||||
root_uri: start_request.root_uri.clone(),
|
||||
workspace_id: start_request.workspace_id.clone(),
|
||||
page_path: start_request.page_path.clone(),
|
||||
@@ -5315,6 +5402,22 @@ pub async fn send(
|
||||
request.selected_context.as_ref(),
|
||||
request.context_refs.as_deref(),
|
||||
)?;
|
||||
let command_type = match request
|
||||
.streaming_behavior
|
||||
.as_deref()
|
||||
.map(str::trim)
|
||||
.filter(|value| !value.is_empty())
|
||||
{
|
||||
Some("steer") => "steer",
|
||||
Some("follow-up") | Some("followUp") | Some("follow_up") => "follow_up",
|
||||
Some(value) => {
|
||||
return Err(WebError::bad_request_code(
|
||||
"page_ai_pi_lab_invalid_streaming_behavior",
|
||||
format!("streamingBehavior 只能是 steer/follow-up,当前为 {value}"),
|
||||
));
|
||||
}
|
||||
None => "prompt",
|
||||
};
|
||||
let command_message = pi_lab_command_message_for_session(
|
||||
&command_session,
|
||||
&request.message,
|
||||
@@ -5323,7 +5426,7 @@ pub async fn send(
|
||||
let plan_mode_prompt_applied = session_permission_mode(&command_session) == Some("plan");
|
||||
let mut command = json!({
|
||||
"id": generate_id("pi_rpc"),
|
||||
"type": "prompt",
|
||||
"type": command_type,
|
||||
"message": command_message,
|
||||
"displayMessage": request.message,
|
||||
"context": {
|
||||
@@ -5336,13 +5439,8 @@ pub async fn send(
|
||||
"selectedContext": request.selected_context,
|
||||
},
|
||||
});
|
||||
if let Some(streaming_behavior) = request
|
||||
.streaming_behavior
|
||||
.as_deref()
|
||||
.map(str::trim)
|
||||
.filter(|value| !value.is_empty())
|
||||
{
|
||||
command["streamingBehavior"] = json!(streaming_behavior);
|
||||
if let Some(images) = request.images.clone().filter(|images| !images.is_empty()) {
|
||||
command["images"] = json!(images);
|
||||
}
|
||||
if command_session.runtime_mode == "mock" {
|
||||
publish_event(
|
||||
@@ -5418,7 +5516,6 @@ pub async fn abort(
|
||||
let session = get_session_for_context(&state, &context, &request.session_id)?;
|
||||
if session.runtime_mode != "mock" {
|
||||
let _ = send_rpc_command(&request.session_id, json!({"type": "abort"})).await;
|
||||
let _ = kill_session_process(&request.session_id).await;
|
||||
}
|
||||
update_session(&request.session_id, |session| {
|
||||
session.status = PiLabSessionStatus::Aborted;
|
||||
@@ -5462,6 +5559,60 @@ pub async fn abort(
|
||||
})))
|
||||
}
|
||||
|
||||
pub async fn rpc_command(
|
||||
State(state): State<AppState>,
|
||||
Extension(context): Extension<RequestContext>,
|
||||
Json(request): Json<PiLabRpcCommandRequest>,
|
||||
) -> Result<Json<Value>, WebError> {
|
||||
ensure_enabled(&state)?;
|
||||
cleanup_expired_sessions();
|
||||
ensure_authenticated(&state, &context)?;
|
||||
let session = get_session_for_context(&state, &context, &request.session_id)?;
|
||||
if !session_runtime_is_usable(&session) {
|
||||
return Err(WebError::bad_request_code(
|
||||
"page_ai_pi_lab_runtime_not_started",
|
||||
"Pi runtime 未启动;请先调用 /api/page-ai/pi/start",
|
||||
));
|
||||
}
|
||||
let command = build_pi_rpc_command(&request.command_type, request.params)?;
|
||||
let command_type = command
|
||||
.get("type")
|
||||
.and_then(Value::as_str)
|
||||
.unwrap_or("unknown")
|
||||
.to_string();
|
||||
if session.runtime_mode == "mock" {
|
||||
return Ok(Json(json!({
|
||||
"ok": true,
|
||||
"schema": PI_LAB_SCHEMA_RPC_COMMAND,
|
||||
"sessionId": request.session_id,
|
||||
"providerSessionId": session.provider_session_id,
|
||||
"command": command_type,
|
||||
"stateSource": "mock_runtime_snapshot",
|
||||
"rpcResponsePending": false,
|
||||
"response": {
|
||||
"type": "response",
|
||||
"command": command_type,
|
||||
"success": true,
|
||||
"data": Value::Null,
|
||||
},
|
||||
})));
|
||||
}
|
||||
let timeout = Duration::from_millis(request.timeout_ms.unwrap_or(10_000).clamp(1_000, 120_000));
|
||||
let response = send_rpc_command_wait(&request.session_id, command, timeout).await?;
|
||||
Ok(Json(json!({
|
||||
"ok": response.as_ref().is_some_and(|value| {
|
||||
value.get("success").and_then(Value::as_bool).unwrap_or(false)
|
||||
}),
|
||||
"schema": PI_LAB_SCHEMA_RPC_COMMAND,
|
||||
"sessionId": request.session_id,
|
||||
"providerSessionId": session.provider_session_id,
|
||||
"command": command_type,
|
||||
"stateSource": "pi_rpc_command_response",
|
||||
"rpcResponsePending": response.is_none(),
|
||||
"response": response,
|
||||
})))
|
||||
}
|
||||
|
||||
/// POST /api/page-ai/pi/state
|
||||
/// 封装官方 Pi RPC `get_state`。
|
||||
/// mock 返回完整假数据;real 发送 get_state RPC 并等待响应,超时降级。
|
||||
@@ -6616,9 +6767,7 @@ fn pi_lab_entry_parent_id(entry: &Value) -> Option<&str> {
|
||||
}
|
||||
|
||||
fn pi_lab_entry_message(entry: &Value) -> Option<&Value> {
|
||||
entry
|
||||
.get("message")
|
||||
.filter(|message| message.is_object())
|
||||
entry.get("message").filter(|message| message.is_object())
|
||||
}
|
||||
|
||||
fn pi_lab_entry_role(entry: &Value) -> String {
|
||||
@@ -7893,11 +8042,30 @@ pub async fn rename_session(
|
||||
update_session(&path.session_id, |session| {
|
||||
session.page_title = Some(title.to_string());
|
||||
});
|
||||
let mut pi_rpc_response = Value::Null;
|
||||
if let Some(session) = get_session(&path.session_id) {
|
||||
if session.runtime_mode != "mock" && session_runtime_is_usable(&session) {
|
||||
if let Some(response) = send_rpc_command_wait(
|
||||
&path.session_id,
|
||||
json!({
|
||||
"id": generate_id("pi_rpc_set_session_name"),
|
||||
"type": "set_session_name",
|
||||
"name": title,
|
||||
}),
|
||||
Duration::from_secs(5),
|
||||
)
|
||||
.await?
|
||||
{
|
||||
pi_rpc_response = response;
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(Json(json!({
|
||||
"ok": true,
|
||||
"schema": "mnote.page_ai_pi.rename_session.v1",
|
||||
"sessionId": path.session_id,
|
||||
"title": title,
|
||||
"piRpcResponse": pi_rpc_response,
|
||||
"updatedRuns": renamed.len(),
|
||||
})))
|
||||
}
|
||||
@@ -8331,10 +8499,16 @@ mod tests {
|
||||
assert_eq!(replay[0]["role"], "user");
|
||||
assert_eq!(replay[0]["text"], "请查看当前页");
|
||||
assert_eq!(replay[1]["role"], "assistant");
|
||||
assert_eq!(replay[1]["toolCalls"][0]["toolName"], "mnote_current_page_read");
|
||||
assert_eq!(
|
||||
replay[1]["toolCalls"][0]["toolName"],
|
||||
"mnote_current_page_read"
|
||||
);
|
||||
assert_eq!(replay[2]["role"], "assistant");
|
||||
assert_eq!(replay[2]["toolCalls"][0]["status"], "done");
|
||||
assert_eq!(replay[2]["toolCalls"][0]["result"]["content"][0]["text"], "当前页内容");
|
||||
assert_eq!(
|
||||
replay[2]["toolCalls"][0]["result"]["content"][0]["text"],
|
||||
"当前页内容"
|
||||
);
|
||||
assert_eq!(replay[3]["text"], "FINAL_TAIL_REPLY");
|
||||
}
|
||||
|
||||
@@ -8743,6 +8917,30 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pi_rpc_command_wrapper_allows_only_official_control_commands() {
|
||||
let command = build_pi_rpc_command(
|
||||
"getAvailableModels",
|
||||
json!({
|
||||
"ignoredClientField": true
|
||||
}),
|
||||
)
|
||||
.expect("allowed command");
|
||||
assert_eq!(command["type"], "get_available_models");
|
||||
assert!(command["id"]
|
||||
.as_str()
|
||||
.unwrap_or("")
|
||||
.starts_with("pi_rpc_command"));
|
||||
assert!(build_pi_rpc_command("bash", json!({"command": "pwd"})).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pi_rust_start_keeps_official_prompt_templates_and_context_files_enabled() {
|
||||
let source = include_str!("page_ai_pi.rs");
|
||||
assert!(!source.contains(".arg(\"--no-prompt-templates\")"));
|
||||
assert!(!source.contains(".arg(\"--no-context-files\")"));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn list_sessions_filters_by_current_user_and_pi_profile() {
|
||||
let app = test_app();
|
||||
@@ -8893,7 +9091,11 @@ mod tests {
|
||||
assert_eq!(policy["defaultModel"], "omniroute/pi-fast");
|
||||
assert_eq!(
|
||||
policy["allowedModels"],
|
||||
json!(["omniroute/freefirst", "omniroute/gpt-5.4-mini", "omniroute/pi-fast"])
|
||||
json!([
|
||||
"omniroute/freefirst",
|
||||
"omniroute/gpt-5.4-mini",
|
||||
"omniroute/pi-fast"
|
||||
])
|
||||
);
|
||||
assert!(policy["enabledSkills"]
|
||||
.as_array()
|
||||
|
||||
Reference in New Issue
Block a user