@aiti-ai/aiti-omp-agent 0.0.0-stage → 0.1.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/external-governance.ts +138 -0
- package/fork-rpc/cli-resolve.ts +79 -0
- package/fork-rpc/remote-queue.ts +140 -0
- package/fork-rpc/spec.ts +106 -0
- package/fork-rpc/wire.ts +367 -0
- package/host-tools-bridge.ts +89 -0
- package/index.ts +128 -0
- package/omp-handle.test.mjs +465 -0
- package/omp-handle.ts +586 -0
- package/package.json +19 -4
- package/reconcile-deps.mjs +41 -0
- package/reload-instances.mjs +60 -0
- package/tools/deploy.mjs +44 -0
- package/README.md +0 -3
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 aiti-ai contributors
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* external-cli-governance —— 外部 CLI provider 的治理模式(共享源,复制到各扩展目录部署)
|
|
3
|
+
*
|
|
4
|
+
* 来源:pi-subagents 0.58 外部 runner 的治理模式抄写(MIT),适配本仓库 provider 槽。
|
|
5
|
+
* 权威源:pi-web-ext/_shared/external-cli-governance.ts(改动后同步到各扩展副本)。
|
|
6
|
+
* 当前副本:aiti-{claude-code,codex,pi,acp}-agent/external-governance.ts
|
|
7
|
+
*
|
|
8
|
+
* 四件治理:
|
|
9
|
+
* 1. env allowlist(governanceEnv)——spawn 只透传白名单环境变量,父进程的
|
|
10
|
+
* 其他密钥(各家 API key/auth token)不再泄入子 CLI 进程。
|
|
11
|
+
* 2. preflight(preflightCli)——启动前探测二进制存在 + 版本(带 TTL 缓存),
|
|
12
|
+
* fail-loud 提示替代运行时 spawn ENOENT。
|
|
13
|
+
* 3. 能力收窄文案表(CAPABILITY_NOTICE 模式)——不支持的能力带人话理由
|
|
14
|
+
* (我们 provider 槽 capabilities 布尔位之上的文案层,guard 消费后续接)。
|
|
15
|
+
* 4. 读写分型(READONLY_TOOL_ARGS 模式)——只读变体显式声明(-writer 后缀
|
|
16
|
+
* 反向约定:默认只读,writer 才放开写)。
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
import { execFile } from "node:child_process";
|
|
20
|
+
|
|
21
|
+
// ---------------------------------------------------------------------------
|
|
22
|
+
// 1) env allowlist
|
|
23
|
+
// ---------------------------------------------------------------------------
|
|
24
|
+
|
|
25
|
+
/** 所有外部 CLI 通用的基础白名单(系统定位 + 代理 + TLS)。 */
|
|
26
|
+
export const BASE_ENV_ALLOW = [
|
|
27
|
+
"PATH", "HOME", "USERPROFILE", "USER", "LOGNAME", "TMPDIR", "TEMP", "TMP",
|
|
28
|
+
"LANG", "LC_ALL", "TERM",
|
|
29
|
+
"HTTP_PROXY", "HTTPS_PROXY", "NO_PROXY",
|
|
30
|
+
"http_proxy", "https_proxy", "no_proxy",
|
|
31
|
+
"SSL_CERT_FILE", "SSL_CERT_DIR",
|
|
32
|
+
] as const;
|
|
33
|
+
|
|
34
|
+
/** 各 CLI 专属键(与 pi-subagents claude-code adapter 对齐 + 本仓库常用 provider)。 */
|
|
35
|
+
export const CLI_ENV_ALLOW: Record<string, string[]> = {
|
|
36
|
+
"claude-code": [
|
|
37
|
+
"CLAUDE_CONFIG_DIR",
|
|
38
|
+
"ANTHROPIC_API_KEY", "ANTHROPIC_AUTH_TOKEN", "ANTHROPIC_BASE_URL",
|
|
39
|
+
"CLAUDE_CODE_OAUTH_TOKEN", "CLAUDE_CODE_USE_BEDROCK", "CLAUDE_CODE_USE_VERTEX",
|
|
40
|
+
"CLAUDE_CODE_USE_FOUNDRY",
|
|
41
|
+
"AWS_PROFILE", "AWS_REGION", "AWS_DEFAULT_REGION",
|
|
42
|
+
"AWS_ACCESS_KEY_ID", "AWS_SECRET_ACCESS_KEY", "AWS_SESSION_TOKEN", "AWS_BEARER_TOKEN_BEDROCK",
|
|
43
|
+
"GOOGLE_APPLICATION_CREDENTIALS", "CLOUD_ML_REGION", "ANTHROPIC_VERTEX_PROJECT_ID",
|
|
44
|
+
],
|
|
45
|
+
"codex": [
|
|
46
|
+
"OPENAI_API_KEY", "OPENAI_BASE_URL", "CODEX_HOME", "CODEX_API_KEY",
|
|
47
|
+
// 自定义 provider 认证(~/.codex/config.toml 的 model_providers.*.env_key),
|
|
48
|
+
// 如 openrouter 桥接 provider——缺了它轮次直接 turn.failed(Missing environment variable)。
|
|
49
|
+
"OPENROUTER_API_KEY",
|
|
50
|
+
],
|
|
51
|
+
"pi": [
|
|
52
|
+
// pi 自身及其 provider 认证走 agent 目录(models.json/auth.json),
|
|
53
|
+
// 环境变量只留运行时与常见 provider 前缀(防误伤自定义 provider)。
|
|
54
|
+
"PI_CODING_AGENT_DIR", "PI_WEB_PKG_DIR",
|
|
55
|
+
"ANTHROPIC_API_KEY", "OPENAI_API_KEY", "GOOGLE_API_KEY", "GEMINI_API_KEY",
|
|
56
|
+
"ZAI_API_KEY", "DEEPSEEK_API_KEY", "MOONSHOT_API_KEY", "KIMI_API_KEY",
|
|
57
|
+
"QWEN_API_KEY", "DASHSCOPE_API_KEY", "OPENROUTER_API_KEY",
|
|
58
|
+
"SILICONFLOW_API_KEY", "XAI_API_KEY", "GROQ_API_KEY", "MISTRAL_API_KEY",
|
|
59
|
+
],
|
|
60
|
+
"acp": [
|
|
61
|
+
"ACP_*",
|
|
62
|
+
],
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* 生成治理后的子进程 env:白名单键(精确 + `前缀*` 通配)+ extra 覆盖。
|
|
67
|
+
* 替代 `env: { ...process.env }` 的全量透传。
|
|
68
|
+
*/
|
|
69
|
+
export function governanceEnv(cli: keyof typeof CLI_ENV_ALLOW | string, extra?: Record<string, string>): Record<string, string> {
|
|
70
|
+
const allow = [...BASE_ENV_ALLOW, ...(CLI_ENV_ALLOW[cli as string] ?? [])];
|
|
71
|
+
const out: Record<string, string> = {};
|
|
72
|
+
for (const k of Object.keys(process.env)) {
|
|
73
|
+
const hit = allow.some((a) => (a.endsWith("*") ? k.startsWith(a.slice(0, -1)) : k === a));
|
|
74
|
+
if (hit && process.env[k] !== undefined) out[k] = process.env[k] as string;
|
|
75
|
+
}
|
|
76
|
+
return { ...out, ...(extra ?? {}) };
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// ---------------------------------------------------------------------------
|
|
80
|
+
// 2) preflight
|
|
81
|
+
// ---------------------------------------------------------------------------
|
|
82
|
+
|
|
83
|
+
const preflightCache = new Map<string, { at: number; version: string | null; ok: boolean }>();
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* 启动前探测:二进制可执行 + 可选版本(`--version`)。TTL 缓存,失败不缓存负面结果以外的信息。
|
|
87
|
+
* 返回 { ok, version, error }——ok=false 时 error 是给人看的失败原因(fail-loud)。
|
|
88
|
+
*/
|
|
89
|
+
export function preflightCli(bin: string, opts: { versionArgs?: string[]; timeoutMs?: number; ttlMs?: number } = {}): Promise<{ ok: boolean; version: string | null; error?: string }> {
|
|
90
|
+
const ttl = opts.ttlMs ?? 60_000;
|
|
91
|
+
const cached = preflightCache.get(bin);
|
|
92
|
+
if (cached && Date.now() - cached.at < ttl) {
|
|
93
|
+
return Promise.resolve(cached.ok
|
|
94
|
+
? { ok: true, version: cached.version }
|
|
95
|
+
: { ok: false, version: null, error: cached.version ?? `${bin} 探测失败` });
|
|
96
|
+
}
|
|
97
|
+
return new Promise((resolve) => {
|
|
98
|
+
execFile(bin, opts.versionArgs ?? ["--version"], { timeout: opts.timeoutMs ?? 5_000 }, (err, stdout) => {
|
|
99
|
+
if (err) {
|
|
100
|
+
const msg = String((err as NodeJS.ErrnoException).code) === "ENOENT"
|
|
101
|
+
? `${bin} 未安装或不在 PATH`
|
|
102
|
+
: `${bin} 探测失败: ${err.message.split("\n")[0]}`;
|
|
103
|
+
preflightCache.set(bin, { at: Date.now(), version: msg, ok: false });
|
|
104
|
+
resolve({ ok: false, version: null, error: msg });
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
const version = String(stdout).trim().split("\n")[0]?.slice(0, 64) ?? null;
|
|
108
|
+
preflightCache.set(bin, { at: Date.now(), version, ok: true });
|
|
109
|
+
resolve({ ok: true, version });
|
|
110
|
+
});
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// ---------------------------------------------------------------------------
|
|
115
|
+
// 3) 能力收窄文案表(模式示例——provider 按需声明,guard 消费后续接)
|
|
116
|
+
// ---------------------------------------------------------------------------
|
|
117
|
+
|
|
118
|
+
/** 不支持能力的人话理由(替代裸 false;provider 的 capabilities 旁路声明)。 */
|
|
119
|
+
export interface CapabilityNotices {
|
|
120
|
+
[capability: string]: string;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/** one-shot stdin 型外部 CLI 的通用收窄理由(pi-subagents external-cli-contract 对齐)。 */
|
|
124
|
+
export const ONESHOT_CLI_NOTICES: CapabilityNotices = {
|
|
125
|
+
steer: "one-shot stdin 模式在启动后关闭输入,无法接受运行中导向消息。",
|
|
126
|
+
fork: "外部 CLI 会话无原生 fork 语义(无 pi session 树)。",
|
|
127
|
+
compact: "外部 CLI 不暴露上下文压缩接口。",
|
|
128
|
+
setActiveTools: "外部 CLI 的工具白名单只能启动时声明,不能运行中切换。",
|
|
129
|
+
};
|
|
130
|
+
|
|
131
|
+
// ---------------------------------------------------------------------------
|
|
132
|
+
// 4) 读写分型
|
|
133
|
+
// ---------------------------------------------------------------------------
|
|
134
|
+
|
|
135
|
+
/** 只读变体的 CLI 参数(writer 变体反向约定:默认只读,writer 才放开写)。 */
|
|
136
|
+
export const READONLY_TOOL_ARGS: Record<string, string[]> = {
|
|
137
|
+
"claude-code": ["--allowedTools", "Read,Glob,Grep"],
|
|
138
|
+
};
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* cli-resolve.ts — fork CLI 解析(载体无关)。
|
|
3
|
+
* 拆自 aiti-pi-agent 的 pi-rpc-spawn.ts:resolveForkCli / resolveBun / which。
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { existsSync, statSync } from "node:fs";
|
|
7
|
+
import { join } from "node:path";
|
|
8
|
+
import type { PiForkSpec } from "./spec.ts";
|
|
9
|
+
|
|
10
|
+
export interface ResolvedForkCli {
|
|
11
|
+
/** 实际执行的解释器(node / bun)。 */
|
|
12
|
+
runner: string;
|
|
13
|
+
/** 解释器参数(通常 [cliPath];PATH 可执行直跑时 [])。 */
|
|
14
|
+
args: string[];
|
|
15
|
+
/** 人类可读的解析来源(日志/报错用)。 */
|
|
16
|
+
origin: string;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* 解析 fork CLI。解析顺序:
|
|
21
|
+
* 1) spec.cliEnvVar 显式指定(绝对路径,交给 runtime 解释)
|
|
22
|
+
* 2) spec.bundledCli()(pi:pi-web 包内 SDK cli.js,node 解释)
|
|
23
|
+
* 3) PATH 上的 spec.pathBin(绝对路径 → runtime 解释;找不到 → null)
|
|
24
|
+
*
|
|
25
|
+
* fork 未装时返回 null:provider 照常注册,createSession 报清晰错误(不阻塞列表)。
|
|
26
|
+
*/
|
|
27
|
+
export function resolveForkCli(spec: PiForkSpec): ResolvedForkCli | null {
|
|
28
|
+
const explicit = process.env[spec.cliEnvVar];
|
|
29
|
+
if (explicit && existsSync(explicit)) {
|
|
30
|
+
return { runner: spec.runtime === "bun" ? resolveBun() : process.execPath, args: [explicit], origin: `env ${spec.cliEnvVar}` };
|
|
31
|
+
}
|
|
32
|
+
const bundled = spec.bundledCli?.();
|
|
33
|
+
if (bundled) {
|
|
34
|
+
return { runner: process.execPath, args: [bundled], origin: "bundled SDK" };
|
|
35
|
+
}
|
|
36
|
+
const onPath = which(spec.pathBin);
|
|
37
|
+
if (onPath) {
|
|
38
|
+
return { runner: spec.runtime === "bun" ? resolveBun() : process.execPath, args: [onPath], origin: `PATH ${spec.pathBin}` };
|
|
39
|
+
}
|
|
40
|
+
// 兜底:bun 全局安装目录(systemd 等服务进程 PATH 通常不含 ~/.bun/bin)
|
|
41
|
+
const bunGlobalBin = join(process.env.HOME || "", ".bun", "bin", spec.pathBin);
|
|
42
|
+
if (spec.runtime === "bun" && existsSync(bunGlobalBin)) {
|
|
43
|
+
return { runner: resolveBun(), args: [bunGlobalBin], origin: "~/.bun/bin" };
|
|
44
|
+
}
|
|
45
|
+
return null;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** bun 解释器解析:env BUN → PATH bun。找不到返回 "bun"(让 spawn 报 ENOENT,fail loud)。 */
|
|
49
|
+
export function resolveBun(): string {
|
|
50
|
+
return process.env.BUN || "bun";
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** PATH 查找可执行文件(同步;Windows 兼容:`;` 分隔 + PATHEXT 后缀 + isFile 校验;
|
|
54
|
+
* POSIX 额外要求可执行位)。找不到返回 undefined。 */
|
|
55
|
+
export function which(bin: string): string | undefined {
|
|
56
|
+
const isWin = process.platform === "win32";
|
|
57
|
+
const pathDirs = (process.env.PATH ?? "").split(isWin ? ";" : ":").filter(Boolean);
|
|
58
|
+
// Windows 还要试 PATHEXT 后缀(pi.cmd / pi.exe …);首项空串 = 精确名优先
|
|
59
|
+
const exts = isWin ? ["", ...(process.env.PATHEXT ?? ".COM;.EXE;.BAT;.CMD").split(";")] : [""];
|
|
60
|
+
for (const dir of pathDirs) {
|
|
61
|
+
for (const ext of exts) {
|
|
62
|
+
const candidate = join(dir, bin + ext);
|
|
63
|
+
if (isExecutableFile(candidate)) return candidate;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
return undefined;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/** 候选必须是文件(目录命名的同名项不算)且可执行(POSIX 查可执行位)。 */
|
|
70
|
+
function isExecutableFile(path: string): boolean {
|
|
71
|
+
let st;
|
|
72
|
+
try {
|
|
73
|
+
st = statSync(path);
|
|
74
|
+
} catch {
|
|
75
|
+
return false;
|
|
76
|
+
}
|
|
77
|
+
if (!st.isFile()) return false;
|
|
78
|
+
return process.platform === "win32" || (st.mode & 0o111) !== 0;
|
|
79
|
+
}
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* remote-queue — 远端 pi 协议队列的 queueManage 适配层(共享层,副本随扩展部署——改动须双向同步;
|
|
3
|
+
* docs/aiti-remote-queue-and-sidecar-plan.md §2)。
|
|
4
|
+
*
|
|
5
|
+
* 适用形态:pi RPC 子进程(aiti-pi-agent)与其超集 fork omp(aiti-omp-agent)——队列由
|
|
6
|
+
* 子进程持有,协议原生支持 clear_queue(返回清空前文本,与 pi-web core clearQueue 同契约)
|
|
7
|
+
* 与 steer/follow_up 重注入;子进程经 RPC 流转发 queue_update 帧(含完整文本数组)。
|
|
8
|
+
*
|
|
9
|
+
* 镜像正确性:handle 生命周期 == 子进程生命周期,双方同起点(皆空),无重连缺口——
|
|
10
|
+
* 镜像只从 observeFrame 喂入的 queue_update 帧维护。get_state 仅有 pendingMessageCount
|
|
11
|
+
* (无文本),不存在「拉全量」原语,重连场景不在本层适配范围。
|
|
12
|
+
*
|
|
13
|
+
* 变更类操作语义(与 pi-web core rpc-manager move_queue 同款):镜像算出新序 →
|
|
14
|
+
* clear_queue 全量清空 → 按新序重注入。运行中 steer 重注入可能被立即消费的竞态
|
|
15
|
+
* 为该策略既有语义(clear+re-enqueue 在单请求内原子性足够)。
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
export interface RemoteQueueManageReq {
|
|
19
|
+
action: "list" | "clear" | "move" | "convert" | "remove" | "recall" | "requeue";
|
|
20
|
+
kind?: "steer" | "followUp";
|
|
21
|
+
from?: number;
|
|
22
|
+
to?: number;
|
|
23
|
+
index?: number;
|
|
24
|
+
targetKind?: "steer" | "followUp";
|
|
25
|
+
text?: string;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export interface RemoteQueueManageResult {
|
|
29
|
+
steering: string[];
|
|
30
|
+
followUp: string[];
|
|
31
|
+
entry?: { text: string };
|
|
32
|
+
/** remove 时返回(对齐 backend-queue / pi-web core QueueTrashItem 的文本字段)。 */
|
|
33
|
+
removed?: { text: string };
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export interface RemotePiQueue {
|
|
37
|
+
/** handle.onFrame 里喂入子进程帧;只消费 queue_update(镜像维护,不重复广播——原帧由 handle 照常 emit)。 */
|
|
38
|
+
observeFrame(msg: Record<string, unknown>): void;
|
|
39
|
+
/** SessionHandle.queueManage 直转发。 */
|
|
40
|
+
queueManage(req: RemoteQueueManageReq): Promise<RemoteQueueManageResult>;
|
|
41
|
+
/** 当前镜像快照(steer/followUp 两组文本)。 */
|
|
42
|
+
snapshot(): { steering: string[]; followUp: string[] };
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export function createRemotePiQueueSupport(opts: {
|
|
46
|
+
/** 子进程 clear_queue 请求(返回清空前文本;上游不支持时 fail-loud 由调用方人话化)。 */
|
|
47
|
+
clearRemote(): Promise<{ steering?: string[]; followUp?: string[] }>;
|
|
48
|
+
/** 重注入单条(kind=steer → steer 命令;followUp → follow_up 命令)。 */
|
|
49
|
+
reinject(text: string, kind: "steer" | "followUp"): Promise<void>;
|
|
50
|
+
}): RemotePiQueue {
|
|
51
|
+
let mirror: { steering: string[]; followUp: string[] } = { steering: [], followUp: [] };
|
|
52
|
+
|
|
53
|
+
const snapshot = () => ({ steering: [...mirror.steering], followUp: [...mirror.followUp] });
|
|
54
|
+
|
|
55
|
+
const observeFrame = (msg: Record<string, unknown>): void => {
|
|
56
|
+
if (msg.type !== "queue_update") return;
|
|
57
|
+
mirror = {
|
|
58
|
+
steering: Array.isArray(msg.steering) ? (msg.steering as string[]).map(String) : [],
|
|
59
|
+
followUp: Array.isArray(msg.followUp) ? (msg.followUp as string[]).map(String) : [],
|
|
60
|
+
};
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
/** 变更类共用骨架:对镜像副本计算新序 → 远端全量清空 → 按新序重注入 → 镜像落为新序。 */
|
|
64
|
+
const mutate = async (
|
|
65
|
+
plan: (cur: { steering: string[]; followUp: string[] }) => { steering: string[]; followUp: string[] },
|
|
66
|
+
): Promise<void> => {
|
|
67
|
+
const next = plan(snapshot());
|
|
68
|
+
await opts.clearRemote();
|
|
69
|
+
for (const text of next.steering) await opts.reinject(text, "steer");
|
|
70
|
+
for (const text of next.followUp) await opts.reinject(text, "followUp");
|
|
71
|
+
mirror = { steering: [...next.steering], followUp: [...next.followUp] };
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
const at = (arr: string[], i: number | undefined): number => {
|
|
75
|
+
const n = Number(i);
|
|
76
|
+
if (!Number.isInteger(n) || n < 0 || n >= arr.length) throw new Error("队列索引越界");
|
|
77
|
+
return n;
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
const queueManage = async (req: RemoteQueueManageReq): Promise<RemoteQueueManageResult> => {
|
|
81
|
+
if (req.action === "list") return { ...snapshot() };
|
|
82
|
+
switch (req.action) {
|
|
83
|
+
case "clear": {
|
|
84
|
+
// 远端权威清空:返回值 = 清空前文本(即「清掉了什么」,snapshot 语义不适用)
|
|
85
|
+
const cleared = await opts.clearRemote();
|
|
86
|
+
mirror = { steering: [], followUp: [] };
|
|
87
|
+
return { steering: (cleared.steering ?? []).map(String), followUp: (cleared.followUp ?? []).map(String) };
|
|
88
|
+
}
|
|
89
|
+
case "remove":
|
|
90
|
+
case "recall": {
|
|
91
|
+
const cur = snapshot();
|
|
92
|
+
const group = req.kind === "steer" ? cur.steering : cur.followUp;
|
|
93
|
+
const [picked] = group.splice(at(group, req.index), 1);
|
|
94
|
+
await mutate(() => cur);
|
|
95
|
+
return {
|
|
96
|
+
steering: cur.steering,
|
|
97
|
+
followUp: cur.followUp,
|
|
98
|
+
entry: { text: picked },
|
|
99
|
+
...(req.action === "remove" ? { removed: { text: picked } } : {}),
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
case "move": {
|
|
103
|
+
const cur = snapshot();
|
|
104
|
+
const group = req.kind === "steer" ? cur.steering : cur.followUp;
|
|
105
|
+
// 两个索引都在删除前对原长度校验(pi-web core move_queue 同款语义)
|
|
106
|
+
const from = at(group, req.from);
|
|
107
|
+
const to = at(group, req.to);
|
|
108
|
+
const [moved] = group.splice(from, 1);
|
|
109
|
+
group.splice(to, 0, moved);
|
|
110
|
+
await mutate(() => cur);
|
|
111
|
+
return { steering: cur.steering, followUp: cur.followUp };
|
|
112
|
+
}
|
|
113
|
+
case "requeue": {
|
|
114
|
+
const cur = snapshot();
|
|
115
|
+
const group = req.kind === "steer" ? cur.steering : cur.followUp;
|
|
116
|
+
const i = Number(req.index);
|
|
117
|
+
const text = String(req.text ?? "");
|
|
118
|
+
if (!Number.isInteger(i) || i < 0 || i > group.length) throw new Error("队列索引越界");
|
|
119
|
+
if (!text.trim()) throw new Error("requeue 需要非空文本");
|
|
120
|
+
group.splice(i, 0, text);
|
|
121
|
+
await mutate(() => cur);
|
|
122
|
+
return { steering: cur.steering, followUp: cur.followUp };
|
|
123
|
+
}
|
|
124
|
+
case "convert": {
|
|
125
|
+
const target = req.targetKind === "followUp" ? "followUp" : "steer";
|
|
126
|
+
const kind = req.kind === "steer" ? "steer" : "followUp";
|
|
127
|
+
if (target === kind) throw new Error("convert 需要不同于当前的 targetKind");
|
|
128
|
+
const cur = snapshot();
|
|
129
|
+
const [picked] = (kind === "steer" ? cur.steering : cur.followUp).splice(at(kind === "steer" ? cur.steering : cur.followUp, req.index), 1);
|
|
130
|
+
(target === "steer" ? cur.steering : cur.followUp).push(picked);
|
|
131
|
+
await mutate(() => cur);
|
|
132
|
+
return { steering: cur.steering, followUp: cur.followUp, entry: { text: picked } };
|
|
133
|
+
}
|
|
134
|
+
default:
|
|
135
|
+
throw new Error(`queueManage: 未知 action "${String(req.action)}"`);
|
|
136
|
+
}
|
|
137
|
+
};
|
|
138
|
+
|
|
139
|
+
return { observeFrame, queueManage, snapshot };
|
|
140
|
+
}
|
package/fork-rpc/spec.ts
ADDED
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* spec.ts — pi 系 fork 家族的类型与能力声明(载体无关,无 fork 语义之外的逻辑)。
|
|
3
|
+
* 拆自 aiti-pi-agent 的 pi-rpc-spawn.ts:类型/常量原样迁移;registerFork/listForks
|
|
4
|
+
* (家族注册机制)随拆分退役——每个扩展各自声明自己的 spec。
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
// ============================================================================
|
|
8
|
+
// 家族差异声明
|
|
9
|
+
// ============================================================================
|
|
10
|
+
|
|
11
|
+
export interface PiForkCommands {
|
|
12
|
+
/** fork 元语命令名:pi=fork;omp=branch。 */
|
|
13
|
+
fork: "fork" | "branch";
|
|
14
|
+
/** fork 消息查询命令名(本引擎暂未消费,声明给后续 viewer 用)。 */
|
|
15
|
+
getForkMessages: "get_fork_messages" | "get_branch_messages";
|
|
16
|
+
/** 命令清单查询命令名:pi=get_commands;omp=get_available_commands。 */
|
|
17
|
+
getCommands: "get_commands" | "get_available_commands";
|
|
18
|
+
/** 是否支持 get_entries(pi true;omp false → 走 get_messages)。 */
|
|
19
|
+
hasGetEntries: boolean;
|
|
20
|
+
/** 是否有启动 ready 握手帧(omp true;pi 无 → 300ms sleep 兜底)。 */
|
|
21
|
+
hasReadyFrame: boolean;
|
|
22
|
+
/** get_state 是否含 contextUsage(omp true)。 */
|
|
23
|
+
hasContextUsage: boolean;
|
|
24
|
+
/** get_state 是否含 dumpTools(omp true)。 */
|
|
25
|
+
hasDumpTools: boolean;
|
|
26
|
+
/** 是否支持宿主工具注入(omp set_host_tools / host_tool_call|result|cancel)。 */
|
|
27
|
+
hasHostTools: boolean;
|
|
28
|
+
/** 是否支持 OAuth 登录 RPC(omp get_login_providers/login → open_url 帧;pi 无)。 */
|
|
29
|
+
hasLogin: boolean;
|
|
30
|
+
/** A09:subagent RPC 面(omp get_subagents/set_subagent_subscription/get_subagent_messages;pi 无)。 */
|
|
31
|
+
hasSubagents: boolean;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export interface PiForkSpec {
|
|
35
|
+
/** 家族成员 id(= provider type 后缀:spawn / omp-spawn)。 */
|
|
36
|
+
id: string;
|
|
37
|
+
/** Backend 下拉 UI label。 */
|
|
38
|
+
label: string;
|
|
39
|
+
/** Backend 下拉位次(SessionProvider.order)。 */
|
|
40
|
+
order: number;
|
|
41
|
+
/** agentKey(all-sessions 认领键;omp 会话落 ~/.omp,不认领 pi 历史)。 */
|
|
42
|
+
agentKey?: string;
|
|
43
|
+
/** 执行运行时:node → 显式 process.execPath 解释 cli.js;bun → 显式 bun 解释。 */
|
|
44
|
+
runtime: "node" | "bun";
|
|
45
|
+
/** PATH 上的 CLI 名(pi / omp)。 */
|
|
46
|
+
pathBin: string;
|
|
47
|
+
/** 显式 CLI 覆盖 env 变量名(如 OMP_CLI)。 */
|
|
48
|
+
cliEnvVar: string;
|
|
49
|
+
/** 包内自带 SDK cli.js 路径(pi 专用:PI_WEB_PKG_DIR);无则不启用该解析层。 */
|
|
50
|
+
bundledCli?(): string | undefined;
|
|
51
|
+
commands: PiForkCommands;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* 宿主工具定义(omp host tools):宿主注入给 fork 子进程的工具;agent 调用时经
|
|
56
|
+
* `host_tool_call` 帧回传宿主执行,宿主以 `host_tool_result` 应答(omp 独有能力)。
|
|
57
|
+
*/
|
|
58
|
+
export interface HostToolDefinition {
|
|
59
|
+
name: string;
|
|
60
|
+
label?: string;
|
|
61
|
+
description: string;
|
|
62
|
+
/** JSON Schema(omp RpcHostToolDefinition.parameters)。 */
|
|
63
|
+
parameters: Record<string, unknown>;
|
|
64
|
+
/** 宿主执行体:返回文本结果;抛错 → isError 结果帧。 */
|
|
65
|
+
execute(args: Record<string, unknown>, signal: AbortSignal): Promise<string>;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/** Handle 能力位(SessionCapabilities 的 fork 家族子集,前端据此渲染操作面)。 */
|
|
69
|
+
export interface ForkCapabilities extends Record<string, boolean> {}
|
|
70
|
+
|
|
71
|
+
/** 由 spec.commands 派生能力位(pi/omp 各扩展以此声明 maxCapabilities)。 */
|
|
72
|
+
export function capabilitiesFor(spec: PiForkSpec): ForkCapabilities {
|
|
73
|
+
return {
|
|
74
|
+
steer: true,
|
|
75
|
+
followUp: true,
|
|
76
|
+
// 队列 keeper 全 CRUD(remote-queue 共享层:clear_queue 原生 + clear/重注入变更策略,
|
|
77
|
+
// docs/aiti-remote-queue-and-sidecar-plan.md §2)
|
|
78
|
+
queueManage: true,
|
|
79
|
+
// steering 条目同样可撤回/移动/转换(变更即 clear+重注入)——宿主据此放开队列面板
|
|
80
|
+
// steer 行的行级操作;zcode 那类「已注入对端进程、协议收不回」的后端不声明
|
|
81
|
+
steerQueueManage: true,
|
|
82
|
+
getMessages: true,
|
|
83
|
+
contextUsage: spec.commands.hasContextUsage, // omp get_state.contextUsage;pi RPC 无
|
|
84
|
+
setModel: true,
|
|
85
|
+
setThinkingLevel: true,
|
|
86
|
+
fork: true,
|
|
87
|
+
renameSession: true,
|
|
88
|
+
compact: true,
|
|
89
|
+
executeBash: true,
|
|
90
|
+
setActiveTools: false, // 两系 RPC 均无(待实测协议面,见拆分计划 B5)
|
|
91
|
+
navigateTree: false,
|
|
92
|
+
deleteSession: false,
|
|
93
|
+
getCommands: true,
|
|
94
|
+
listModels: true,
|
|
95
|
+
listTools: spec.commands.hasDumpTools, // omp get_state.dumpTools;pi RPC 无(待实测)
|
|
96
|
+
// 进阶能力(2026-09-14 拆分时盘点):respondInteraction 已实现,补声明;
|
|
97
|
+
// respondPermission/getUsage/fileAttachments 待实测协议面后补(B5)。
|
|
98
|
+
respondInteraction: true,
|
|
99
|
+
subagents: spec.commands.hasSubagents,
|
|
100
|
+
respondPermission: false,
|
|
101
|
+
getUsage: false,
|
|
102
|
+
fileAttachments: false,
|
|
103
|
+
respondProviderHeaders: false, // zcode 专属
|
|
104
|
+
setPermissionMode: false, // cc-connect 专属
|
|
105
|
+
};
|
|
106
|
+
}
|