fix: align pi rust p1 official features
This commit is contained in:
@@ -0,0 +1,30 @@
|
|||||||
|
# 7-63 Page AI Pi Rust Official P1 Alignment Checklist
|
||||||
|
|
||||||
|
## 背景
|
||||||
|
|
||||||
|
Pi Rust built-in tools 已恢复为一等工具面,但 MNote Pi Lab 仍有一组官方能力只接了外壳、被 MNote 自造路径替代,或没有 UI/API 入口。该缺陷以 Pi Rust 官方 RPC/CLI 语义为准,不再把 MNote 当成 Pi 能力替代层;MNote 只负责展示、授权目录、URL/引用与宿主上下文。
|
||||||
|
|
||||||
|
## Checklist
|
||||||
|
|
||||||
|
- [x] 官方 RPC 管理面:接入受控 `rpc-command` endpoint,覆盖 `get_messages`、`get_available_models`、`new_session`、`switch_session`、`set_session_name`、`export_html`、`abort_bash`。
|
||||||
|
- [x] 中途发送:`steer` / `follow_up` 使用官方 RPC command,普通 prompt 保留为默认 `prompt`。
|
||||||
|
- [x] Abort 语义:普通 abort 只发送官方 `abort`,不直接 kill runtime;新建会话/切换模式等需要结束旧进程的路径另走明确 stop/kill。
|
||||||
|
- [x] Bash 中止:full access 下长 bash 暴露 `abort_bash` UI/API。
|
||||||
|
- [x] Prompt templates:移除默认 `--no-prompt-templates`,无配置时允许官方 `.pi/prompts` / `~/.pi/agent/prompts` 发现。
|
||||||
|
- [x] `@file` / context files:移除默认 `--no-context-files`,让 Pi Rust 官方 `@file` 与资源扩展处理真实文件地址;MNote hidden context 仅作为页面/selection/allowed roots 附加上下文。
|
||||||
|
- [x] 图片/附件:前端接入文件选择并按 Pi RPC `images` base64 payload 传给 `prompt`/`steer`/`follow_up`。
|
||||||
|
- [x] 测试:补静态 smoke 覆盖启动参数、RPC command allowlist、图片 payload、abort 不 kill;补浏览器 smoke 覆盖附件入口、RPC command wrapper 和官方 built-in tools 不回退。
|
||||||
|
- [x] 验证:`cargo test -p mnote-web page_ai_pi`、相关 `scripts/task-pi-lab-*.js`、真实 Chromium smoke。
|
||||||
|
- [x] 完成后移动到 `bugs/07-ai/done/` 并记录提交与验证证据。
|
||||||
|
|
||||||
|
## 验证证据
|
||||||
|
|
||||||
|
- `cargo test -p mnote-web page_ai_pi -- --nocapture`:30 passed。
|
||||||
|
- `node scripts/task-pi-lab-static-smoke.js`:253 checks passed。
|
||||||
|
- `MNOTE_UI_BASE_URL=http://127.0.0.1:3000 PLAYWRIGHT_CHROMIUM_EXECUTABLE=/usr/bin/chromium-browser node scripts/task-pi-lab-full-access-builtin-delete-smoke.js`:通过,结果 `/tmp/mnote-pi-full-access-builtin-tools-1783775113182/result.json`。
|
||||||
|
|
||||||
|
## 非目标
|
||||||
|
|
||||||
|
- 不在本轮开放任意外部 Pi extension/package install。
|
||||||
|
- 不改变 MNote allowed roots / permission mode 的宿主授权边界。
|
||||||
|
- 不把 OpenHub/LightRAG/MCP 的产品策略混入官方 Pi Rust RPC 对齐。
|
||||||
@@ -28,6 +28,7 @@
|
|||||||
START: '/api/page-ai/pi/start',
|
START: '/api/page-ai/pi/start',
|
||||||
SEND: '/api/page-ai/pi/send',
|
SEND: '/api/page-ai/pi/send',
|
||||||
ABORT: '/api/page-ai/pi/abort',
|
ABORT: '/api/page-ai/pi/abort',
|
||||||
|
RPC_COMMAND: '/api/page-ai/pi/rpc-command',
|
||||||
UI_RESPONSE: '/api/page-ai/pi/ui-response',
|
UI_RESPONSE: '/api/page-ai/pi/ui-response',
|
||||||
EVENTS: '/api/page-ai/pi/events',
|
EVENTS: '/api/page-ai/pi/events',
|
||||||
BOOTSTRAP: '/api/page-ai/pi/bootstrap',
|
BOOTSTRAP: '/api/page-ai/pi/bootstrap',
|
||||||
@@ -97,6 +98,7 @@
|
|||||||
modelOptions: [],
|
modelOptions: [],
|
||||||
history: [],
|
history: [],
|
||||||
pendingQueue: { steering: [], followUp: [] },
|
pendingQueue: { steering: [], followUp: [] },
|
||||||
|
pendingImages: [],
|
||||||
viewingHistorySessionId: null,
|
viewingHistorySessionId: null,
|
||||||
contextSelection: {
|
contextSelection: {
|
||||||
currentPage: false,
|
currentPage: false,
|
||||||
@@ -1647,6 +1649,10 @@
|
|||||||
window.location.assign('/user/ai#ai-admin-access');
|
window.location.assign('/user/ai#ai-admin-access');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
if (action === 'attach-file' || action === 'camera') {
|
||||||
|
pickPiPromptImages(action === 'camera');
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (action === 'send-steer') {
|
if (action === 'send-steer') {
|
||||||
sendCurrentInput({ streamingBehavior: 'steer' });
|
sendCurrentInput({ streamingBehavior: 'steer' });
|
||||||
return;
|
return;
|
||||||
@@ -1655,6 +1661,14 @@
|
|||||||
sendCurrentInput({ streamingBehavior: 'followUp' });
|
sendCurrentInput({ streamingBehavior: 'followUp' });
|
||||||
return;
|
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') {
|
if (action === 'plan-mode') {
|
||||||
var input = piLabPanelEl && piLabPanelEl.querySelector('[data-page-ai-pi-lab-input]');
|
var input = piLabPanelEl && piLabPanelEl.querySelector('[data-page-ai-pi-lab-input]');
|
||||||
var current = input ? input.value.trim() : '';
|
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-sep"></div>' +
|
||||||
'<div class="wolai-page-ai-pi-lab-menu-section">权限与附件</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="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="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" 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="camera">' + piIcon('camera') + '<span>拍照</span></button>' +
|
||||||
'<div class="wolai-page-ai-pi-lab-menu-sep"></div>' +
|
'<div class="wolai-page-ai-pi-lab-menu-sep"></div>' +
|
||||||
'<div class="wolai-page-ai-pi-lab-menu-section">执行中发送</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-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="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>' +
|
'<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>' +
|
||||||
'<div class="wolai-page-ai-pi-lab-modebar" data-page-ai-pi-lab-modebar>' +
|
'<div class="wolai-page-ai-pi-lab-modebar" data-page-ai-pi-lab-modebar>' +
|
||||||
@@ -3386,6 +3401,71 @@
|
|||||||
}).catch(function () {});
|
}).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) {
|
function responseJsonOrError(response, fallbackMessage) {
|
||||||
return response.json().catch(function () { return {}; }).then(function (payload) {
|
return response.json().catch(function () { return {}; }).then(function (payload) {
|
||||||
@@ -3947,6 +4027,10 @@
|
|||||||
selectedContext: contextSelection,
|
selectedContext: contextSelection,
|
||||||
};
|
};
|
||||||
if (streamingBehavior) body.streamingBehavior = streamingBehavior;
|
if (streamingBehavior) body.streamingBehavior = streamingBehavior;
|
||||||
|
if (piLabState.pendingImages && piLabState.pendingImages.length) {
|
||||||
|
body.images = piLabState.pendingImages;
|
||||||
|
piLabState.pendingImages = [];
|
||||||
|
}
|
||||||
return fetch(API.SEND, {
|
return fetch(API.SEND, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
credentials: 'same-origin',
|
credentials: 'same-origin',
|
||||||
|
|||||||
@@ -1905,7 +1905,10 @@ pub(crate) fn load_effective_ai_runtime_policy(
|
|||||||
.into_iter()
|
.into_iter()
|
||||||
.filter_map(|entry| normalize_model_ref(Some(&default_provider), &entry.id))
|
.filter_map(|entry| normalize_model_ref(Some(&default_provider), &entry.id))
|
||||||
.collect::<Vec<_>>();
|
.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());
|
allowed_models.push(FREEFIRST_PI_MODEL.to_string());
|
||||||
}
|
}
|
||||||
if !allowed_models.iter().any(|model| model == DEFAULT_PI_MODEL) {
|
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/configure", post(page_ai_pi::configure))
|
||||||
.route("/api/page-ai/pi/state", post(page_ai_pi::state))
|
.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/compact", post(page_ai_pi::compact))
|
||||||
|
.route("/api/page-ai/pi/rpc-command", post(page_ai_pi::rpc_command))
|
||||||
.route(
|
.route(
|
||||||
"/api/page-ai/pi/queue-config",
|
"/api/page-ai/pi/queue-config",
|
||||||
post(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_STATE: &str = "mnote.page_ai_pi.state.v1";
|
||||||
const PI_LAB_SCHEMA_COMPACT: &str = "mnote.page_ai_pi.compact.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_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_DEFAULT_MODEL_PROVIDER: &str = "omniroute";
|
||||||
const PI_LAB_SCHEMA_SESSION_TREE: &str = "mnote.page_ai_pi.session_tree.v1";
|
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";
|
const PI_LAB_SCHEMA_FORK: &str = "mnote.page_ai_pi.fork.v1";
|
||||||
@@ -263,6 +264,7 @@ pub struct PiLabSendRequest {
|
|||||||
pub session_id: String,
|
pub session_id: String,
|
||||||
pub message: String,
|
pub message: String,
|
||||||
pub streaming_behavior: Option<String>,
|
pub streaming_behavior: Option<String>,
|
||||||
|
pub images: Option<Vec<Value>>,
|
||||||
pub root_uri: Option<String>,
|
pub root_uri: Option<String>,
|
||||||
pub workspace_id: Option<String>,
|
pub workspace_id: Option<String>,
|
||||||
pub page_path: Option<String>,
|
pub page_path: Option<String>,
|
||||||
@@ -278,6 +280,17 @@ pub struct PiLabAbortRequest {
|
|||||||
pub session_id: String,
|
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)]
|
#[derive(Debug, Deserialize)]
|
||||||
#[serde(rename_all = "camelCase")]
|
#[serde(rename_all = "camelCase")]
|
||||||
pub struct PiLabRenameSessionRequest {
|
pub struct PiLabRenameSessionRequest {
|
||||||
@@ -723,10 +736,7 @@ fn resolve_file_path(
|
|||||||
.filter(|value| !value.is_empty());
|
.filter(|value| !value.is_empty());
|
||||||
let relative_path = if let Some(folder_path) = folder_path {
|
let relative_path = if let Some(folder_path) = folder_path {
|
||||||
let normalized_path = path.replace('\\', "/").trim_start_matches('/').to_string();
|
let normalized_path = path.replace('\\', "/").trim_start_matches('/').to_string();
|
||||||
let normalized_folder = folder_path
|
let normalized_folder = folder_path.replace('\\', "/").trim_matches('/').to_string();
|
||||||
.replace('\\', "/")
|
|
||||||
.trim_matches('/')
|
|
||||||
.to_string();
|
|
||||||
if normalized_folder.is_empty()
|
if normalized_folder.is_empty()
|
||||||
|| Path::new(&normalized_path).is_absolute()
|
|| Path::new(&normalized_path).is_absolute()
|
||||||
|| normalized_path == normalized_folder
|
|| normalized_path == normalized_folder
|
||||||
@@ -2568,7 +2578,10 @@ fn stage_extension_file(
|
|||||||
Ok(target_path.to_string_lossy().to_string())
|
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 target = Path::new(extension_path);
|
||||||
let source = fs::read_to_string(target).map_err(|error| {
|
let source = fs::read_to_string(target).map_err(|error| {
|
||||||
WebError::internal(format!(
|
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
|
let canonical_context_path = context_path
|
||||||
.canonicalize()
|
.canonicalize()
|
||||||
.unwrap_or_else(|_| context_path.to_path_buf());
|
.unwrap_or_else(|_| context_path.to_path_buf());
|
||||||
let context_literal = serde_json::to_string(&canonical_context_path.to_string_lossy().to_string())
|
let context_literal = serde_json::to_string(
|
||||||
.map_err(|error| WebError::internal(format!("编码 Pi Rust MNote context 路径失败: {error}")))?;
|
&canonical_context_path.to_string_lossy().to_string(),
|
||||||
let context_payload: Value = serde_json::from_slice(&fs::read(context_path).map_err(|error| {
|
)
|
||||||
WebError::internal(format!(
|
.map_err(|error| WebError::internal(format!("编码 Pi Rust MNote context 路径失败: {error}")))?;
|
||||||
"读取 Pi Rust MNote context 失败: {}: {error}",
|
let context_payload: Value =
|
||||||
context_path.display()
|
serde_json::from_slice(&fs::read(context_path).map_err(|error| {
|
||||||
))
|
WebError::internal(format!(
|
||||||
})?)
|
"读取 Pi Rust MNote context 失败: {}: {error}",
|
||||||
.map_err(|error| WebError::internal(format!("解析 Pi Rust MNote context 失败: {error}")))?;
|
context_path.display()
|
||||||
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");"#;
|
.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 =
|
let context_snapshot_needle =
|
||||||
r#"const EMBEDDED_CONTEXT: Record<string, unknown> | undefined = undefined;"#;
|
r#"const EMBEDDED_CONTEXT: Record<string, unknown> | undefined = undefined;"#;
|
||||||
if !source.contains(context_file_needle) || !source.contains(context_snapshot_needle) {
|
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(&mnote_pi_extension_path)
|
||||||
.arg("--tools")
|
.arg("--tools")
|
||||||
.arg(pi_tool_names.join(","))
|
.arg(pi_tool_names.join(","))
|
||||||
.arg("--no-prompt-templates")
|
|
||||||
.arg("--no-context-files")
|
|
||||||
.stdin(Stdio::piped())
|
.stdin(Stdio::piped())
|
||||||
.stdout(Stdio::piped())
|
.stdout(Stdio::piped())
|
||||||
.stderr(Stdio::piped());
|
.stderr(Stdio::piped());
|
||||||
@@ -3675,6 +3691,66 @@ fn rpc_response_error_message(response: Option<&Value>, fallback: &str) -> Strin
|
|||||||
.to_string()
|
.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> {
|
fn apply_text_operations(current: &str, operations: &Value) -> Result<String, WebError> {
|
||||||
match operations {
|
match operations {
|
||||||
Value::Array(ops) => {
|
Value::Array(ops) => {
|
||||||
@@ -4149,8 +4225,13 @@ impl PiLabToolFacade {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn local_file_read(&self, params: Value) -> Result<Value, WebError> {
|
fn local_file_read(&self, params: Value) -> Result<Value, WebError> {
|
||||||
let (target, root_uri, relative_path) =
|
let (target, root_uri, relative_path) = resolve_file_path(
|
||||||
resolve_file_path(&self.state, &self.context, ¶ms, self.session.as_ref(), false)?;
|
&self.state,
|
||||||
|
&self.context,
|
||||||
|
¶ms,
|
||||||
|
self.session.as_ref(),
|
||||||
|
false,
|
||||||
|
)?;
|
||||||
let content = fs::read_to_string(&target).map_err(|error| {
|
let content = fs::read_to_string(&target).map_err(|error| {
|
||||||
WebError::bad_request_code(
|
WebError::bad_request_code(
|
||||||
"page_ai_pi_lab_file_read_failed",
|
"page_ai_pi_lab_file_read_failed",
|
||||||
@@ -4168,8 +4249,13 @@ impl PiLabToolFacade {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn local_file_patch(&self, params: Value) -> Result<Value, WebError> {
|
fn local_file_patch(&self, params: Value) -> Result<Value, WebError> {
|
||||||
let (target, root_uri, relative_path) =
|
let (target, root_uri, relative_path) = resolve_file_path(
|
||||||
resolve_file_path(&self.state, &self.context, ¶ms, self.session.as_ref(), true)?;
|
&self.state,
|
||||||
|
&self.context,
|
||||||
|
¶ms,
|
||||||
|
self.session.as_ref(),
|
||||||
|
true,
|
||||||
|
)?;
|
||||||
let before_version = file_version(&target);
|
let before_version = file_version(&target);
|
||||||
let current = fs::read_to_string(&target).unwrap_or_default();
|
let current = fs::read_to_string(&target).unwrap_or_default();
|
||||||
let next = if let Some(content) = params.get("content").and_then(Value::as_str) {
|
let next = if let Some(content) = params.get("content").and_then(Value::as_str) {
|
||||||
@@ -5045,6 +5131,7 @@ pub async fn bootstrap(
|
|||||||
.to_string(),
|
.to_string(),
|
||||||
message: prompt.to_string(),
|
message: prompt.to_string(),
|
||||||
streaming_behavior: None,
|
streaming_behavior: None,
|
||||||
|
images: None,
|
||||||
root_uri: start_request.root_uri.clone(),
|
root_uri: start_request.root_uri.clone(),
|
||||||
workspace_id: start_request.workspace_id.clone(),
|
workspace_id: start_request.workspace_id.clone(),
|
||||||
page_path: start_request.page_path.clone(),
|
page_path: start_request.page_path.clone(),
|
||||||
@@ -5315,6 +5402,22 @@ pub async fn send(
|
|||||||
request.selected_context.as_ref(),
|
request.selected_context.as_ref(),
|
||||||
request.context_refs.as_deref(),
|
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(
|
let command_message = pi_lab_command_message_for_session(
|
||||||
&command_session,
|
&command_session,
|
||||||
&request.message,
|
&request.message,
|
||||||
@@ -5323,7 +5426,7 @@ pub async fn send(
|
|||||||
let plan_mode_prompt_applied = session_permission_mode(&command_session) == Some("plan");
|
let plan_mode_prompt_applied = session_permission_mode(&command_session) == Some("plan");
|
||||||
let mut command = json!({
|
let mut command = json!({
|
||||||
"id": generate_id("pi_rpc"),
|
"id": generate_id("pi_rpc"),
|
||||||
"type": "prompt",
|
"type": command_type,
|
||||||
"message": command_message,
|
"message": command_message,
|
||||||
"displayMessage": request.message,
|
"displayMessage": request.message,
|
||||||
"context": {
|
"context": {
|
||||||
@@ -5336,13 +5439,8 @@ pub async fn send(
|
|||||||
"selectedContext": request.selected_context,
|
"selectedContext": request.selected_context,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
if let Some(streaming_behavior) = request
|
if let Some(images) = request.images.clone().filter(|images| !images.is_empty()) {
|
||||||
.streaming_behavior
|
command["images"] = json!(images);
|
||||||
.as_deref()
|
|
||||||
.map(str::trim)
|
|
||||||
.filter(|value| !value.is_empty())
|
|
||||||
{
|
|
||||||
command["streamingBehavior"] = json!(streaming_behavior);
|
|
||||||
}
|
}
|
||||||
if command_session.runtime_mode == "mock" {
|
if command_session.runtime_mode == "mock" {
|
||||||
publish_event(
|
publish_event(
|
||||||
@@ -5418,7 +5516,6 @@ pub async fn abort(
|
|||||||
let session = get_session_for_context(&state, &context, &request.session_id)?;
|
let session = get_session_for_context(&state, &context, &request.session_id)?;
|
||||||
if session.runtime_mode != "mock" {
|
if session.runtime_mode != "mock" {
|
||||||
let _ = send_rpc_command(&request.session_id, json!({"type": "abort"})).await;
|
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| {
|
update_session(&request.session_id, |session| {
|
||||||
session.status = PiLabSessionStatus::Aborted;
|
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
|
/// POST /api/page-ai/pi/state
|
||||||
/// 封装官方 Pi RPC `get_state`。
|
/// 封装官方 Pi RPC `get_state`。
|
||||||
/// mock 返回完整假数据;real 发送 get_state RPC 并等待响应,超时降级。
|
/// 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> {
|
fn pi_lab_entry_message(entry: &Value) -> Option<&Value> {
|
||||||
entry
|
entry.get("message").filter(|message| message.is_object())
|
||||||
.get("message")
|
|
||||||
.filter(|message| message.is_object())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn pi_lab_entry_role(entry: &Value) -> String {
|
fn pi_lab_entry_role(entry: &Value) -> String {
|
||||||
@@ -7893,11 +8042,30 @@ pub async fn rename_session(
|
|||||||
update_session(&path.session_id, |session| {
|
update_session(&path.session_id, |session| {
|
||||||
session.page_title = Some(title.to_string());
|
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(Json(json!({
|
||||||
"ok": true,
|
"ok": true,
|
||||||
"schema": "mnote.page_ai_pi.rename_session.v1",
|
"schema": "mnote.page_ai_pi.rename_session.v1",
|
||||||
"sessionId": path.session_id,
|
"sessionId": path.session_id,
|
||||||
"title": title,
|
"title": title,
|
||||||
|
"piRpcResponse": pi_rpc_response,
|
||||||
"updatedRuns": renamed.len(),
|
"updatedRuns": renamed.len(),
|
||||||
})))
|
})))
|
||||||
}
|
}
|
||||||
@@ -8331,10 +8499,16 @@ mod tests {
|
|||||||
assert_eq!(replay[0]["role"], "user");
|
assert_eq!(replay[0]["role"], "user");
|
||||||
assert_eq!(replay[0]["text"], "请查看当前页");
|
assert_eq!(replay[0]["text"], "请查看当前页");
|
||||||
assert_eq!(replay[1]["role"], "assistant");
|
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]["role"], "assistant");
|
||||||
assert_eq!(replay[2]["toolCalls"][0]["status"], "done");
|
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");
|
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]
|
#[tokio::test]
|
||||||
async fn list_sessions_filters_by_current_user_and_pi_profile() {
|
async fn list_sessions_filters_by_current_user_and_pi_profile() {
|
||||||
let app = test_app();
|
let app = test_app();
|
||||||
@@ -8893,7 +9091,11 @@ mod tests {
|
|||||||
assert_eq!(policy["defaultModel"], "omniroute/pi-fast");
|
assert_eq!(policy["defaultModel"], "omniroute/pi-fast");
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
policy["allowedModels"],
|
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"]
|
assert!(policy["enabledSkills"]
|
||||||
.as_array()
|
.as_array()
|
||||||
|
|||||||
@@ -272,6 +272,20 @@ async function main() {
|
|||||||
);
|
);
|
||||||
|
|
||||||
await openPiUi(page);
|
await openPiUi(page);
|
||||||
|
result.checks.attachmentButtonsEnabled = await page.locator('[data-page-ai-pi-lab-menu-action="attach-file"]:not([disabled])').count() === 1
|
||||||
|
&& await page.locator('[data-page-ai-pi-lab-menu-action="camera"]:not([disabled])').count() === 1;
|
||||||
|
const rpcState = await requestJson(page, "/api/page-ai/pi/rpc-command", {
|
||||||
|
method: "POST",
|
||||||
|
data: {
|
||||||
|
sessionId: session.sessionId,
|
||||||
|
type: "get_state",
|
||||||
|
params: {},
|
||||||
|
timeoutMs: 10000,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
result.checks.rpcCommandGetStateOk = rpcState.ok === true && rpcState.command === "get_state";
|
||||||
|
assert.equal(result.checks.attachmentButtonsEnabled, true, "Pi RPC 图片附件入口应该可用");
|
||||||
|
assert.equal(result.checks.rpcCommandGetStateOk, true, "受控 Pi RPC command wrapper 应能调用官方 get_state");
|
||||||
await page.screenshot({ path: path.join(OUT, "01-full-access-started.png"), fullPage: false });
|
await page.screenshot({ path: path.join(OUT, "01-full-access-started.png"), fullPage: false });
|
||||||
result.screenshots.started = path.join(OUT, "01-full-access-started.png");
|
result.screenshots.started = path.join(OUT, "01-full-access-started.png");
|
||||||
|
|
||||||
|
|||||||
@@ -204,6 +204,11 @@ const checks = [
|
|||||||
['MNote Pi bridge allows native tools when context file proves Pi Rust', mnotePiExtension.includes('isPiRustNativeRuntime') && mnotePiExtension.includes('Boolean(CONTEXT_FILE)')],
|
['MNote Pi bridge allows native tools when context file proves Pi Rust', mnotePiExtension.includes('isPiRustNativeRuntime') && mnotePiExtension.includes('Boolean(CONTEXT_FILE)')],
|
||||||
['route starts Pi with explicit MNote extension bridge', route.includes('--extension') && route.includes('mnotePiExtension')],
|
['route starts Pi with explicit MNote extension bridge', route.includes('--extension') && route.includes('mnotePiExtension')],
|
||||||
['route starts Pi with MNote tool allowlist', route.includes('--tools') && route.includes('mnoteToolNames') && route.includes('pi_lab_extension_tool_names')],
|
['route starts Pi with MNote tool allowlist', route.includes('--tools') && route.includes('mnoteToolNames') && route.includes('pi_lab_extension_tool_names')],
|
||||||
|
['route keeps Pi Rust official prompt templates and @file context enabled', !route.includes('.arg("--no-prompt-templates")') && !route.includes('.arg("--no-context-files")')],
|
||||||
|
['route exposes controlled Pi Rust RPC command wrapper', route.includes('PI_LAB_SCHEMA_RPC_COMMAND') && route.includes('build_pi_rpc_command') && route.includes('abort_bash') && route.includes('get_available_models')],
|
||||||
|
['route maps mid-stream send to official steer/follow_up commands', route.includes('Some("steer") => "steer"') && route.includes('Some("follow-up") | Some("followUp") | Some("follow_up") => "follow_up"')],
|
||||||
|
['route sends Pi RPC images payload without MNote file substitution', route.includes('pub images: Option<Vec<Value>>') && route.includes('command["images"] = json!(images)')],
|
||||||
|
['route abort uses official RPC without killing runtime process', route.includes('json!({"type": "abort"})') && !route.includes('let _ = kill_session_process(&request.session_id).await;')],
|
||||||
['route passes MNote tool manifest to extension env', route.includes('MNOTE_PI_BRIDGE_TOOLS') && route.includes('PI_MNOTE_BRIDGE_TOOLS') && route.includes('mnote_pi_tool_manifest')],
|
['route passes MNote tool manifest to extension env', route.includes('MNOTE_PI_BRIDGE_TOOLS') && route.includes('PI_MNOTE_BRIDGE_TOOLS') && route.includes('mnote_pi_tool_manifest')],
|
||||||
['route writes dynamic Pi Rust MNote context file', route.includes('mnote.pi.context.v1') && route.includes('PI_MNOTE_CONTEXT_FILE') && route.includes('write_pi_mnote_context_snapshot')],
|
['route writes dynamic Pi Rust MNote context file', route.includes('mnote.pi.context.v1') && route.includes('PI_MNOTE_CONTEXT_FILE') && route.includes('write_pi_mnote_context_snapshot')],
|
||||||
['route binds staged MNote bridge to absolute context path and private embedded snapshot', route.includes('bind_mnote_bridge_context_path') && route.includes('DEFAULT_CONTEXT_FILE') && route.includes('EMBEDDED_CONTEXT')],
|
['route binds staged MNote bridge to absolute context path and private embedded snapshot', route.includes('bind_mnote_bridge_context_path') && route.includes('DEFAULT_CONTEXT_FILE') && route.includes('EMBEDDED_CONTEXT')],
|
||||||
@@ -217,6 +222,8 @@ const checks = [
|
|||||||
['dev:hot warmup defaults to no real model prompt', devHot.includes('MNOTE_PAGE_AI_PI_WARMUP_SEND') && devHot.includes('?? "0"')],
|
['dev:hot warmup defaults to no real model prompt', devHot.includes('MNOTE_PAGE_AI_PI_WARMUP_SEND') && devHot.includes('?? "0"')],
|
||||||
['runtime opens Pi drawer by prestarting current page session', showPiLabBody.includes('startRuntime().then') && showPiLabBody.includes('syncRuntimeState()')],
|
['runtime opens Pi drawer by prestarting current page session', showPiLabBody.includes('startRuntime().then') && showPiLabBody.includes('syncRuntimeState()')],
|
||||||
['runtime send path auto-starts Pi Rust instead of dead-end not-ready toast', runtime.includes('Pi Rust 正在启动,启动后自动发送') && runtime.includes('piLabStartPromise') && !runtime.includes('Pi 会话尚未就绪,请重新打开 Pi 面板或稍后重试')],
|
['runtime send path auto-starts Pi Rust instead of dead-end not-ready toast', runtime.includes('Pi Rust 正在启动,启动后自动发送') && runtime.includes('piLabStartPromise') && !runtime.includes('Pi 会话尚未就绪,请重新打开 Pi 面板或稍后重试')],
|
||||||
|
['runtime enables Pi RPC image attachments', runtime.includes('pendingImages') && runtime.includes('readFileAsPiImage') && runtime.includes('body.images = piLabState.pendingImages') && !runtime.includes('Pi RPC 附件上下文尚未接入')],
|
||||||
|
['runtime exposes official abort_bash command', runtime.includes("RPC_COMMAND: '/api/page-ai/pi/rpc-command'") && runtime.includes("callPiRpcCommand('abort_bash'")],
|
||||||
['runtime keeps selected model before start/configure', runtime.includes('ensurePiToolCapableModel') && !runtime.includes('isPiToolUnsupportedModel') && runtime.includes('gpt-5.4-mini') && runtime.includes('freefirst')],
|
['runtime keeps selected model before start/configure', runtime.includes('ensurePiToolCapableModel') && !runtime.includes('isPiToolUnsupportedModel') && runtime.includes('gpt-5.4-mini') && runtime.includes('freefirst')],
|
||||||
['route applies TTL and rate limits', route.includes('PI_LAB_SESSION_TTL_MS') && route.includes('PI_LAB_RATE_LIMITS') && route.includes('check_rate_limit')],
|
['route applies TTL and rate limits', route.includes('PI_LAB_SESSION_TTL_MS') && route.includes('PI_LAB_RATE_LIMITS') && route.includes('check_rate_limit')],
|
||||||
['route cleans expired session dirs and rate buckets', route.includes('fs::remove_dir_all') && route.includes('buckets.retain')],
|
['route cleans expired session dirs and rate buckets', route.includes('fs::remove_dir_all') && route.includes('buckets.retain')],
|
||||||
|
|||||||
Reference in New Issue
Block a user