@feng0531/dsh-execution-bus 0.0.0-stage → 0.1.1
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 +42 -0
- package/README.md +76 -2
- package/THIRD_PARTY_NOTICES.md +5 -0
- package/dist/bus.d.ts +85 -0
- package/dist/bus.js +328 -0
- package/dist/handlers.d.ts +3 -0
- package/dist/handlers.js +42 -0
- package/dist/plugin.d.ts +9 -0
- package/dist/plugin.js +131 -0
- package/docs/FLOW-TEST.md +38 -0
- package/docs/RELEASING.md +29 -0
- package/docs/host-compatibility.md +44 -0
- package/docs/protocol.md +52 -0
- package/docs/releases/0.1.0.md +17 -0
- package/docs/releases/0.1.1.md +18 -0
- package/docs/validation.md +61 -0
- package/package.json +52 -4
package/LICENSE
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
Execution Bus 非商业源码公开许可证 1.0
|
|
2
|
+
Execution Bus Non-Commercial Source Available License 1.0
|
|
3
|
+
|
|
4
|
+
Copyright (c) 2026 f-e-n-g-0531 及各贡献者。
|
|
5
|
+
项目:https://github.com/f-e-n-g-0531/dsh-execution-bus
|
|
6
|
+
|
|
7
|
+
1. 适用范围
|
|
8
|
+
本许可证适用于本项目中由上述权利人有权授权且未另行标明许可的代码、文档及其修改版本(“本软件”)。本许可证是自定义的源码公开许可证,不是 OSI 认可的开源许可证。中文正文为具有约束力的文本,英文名称仅供识别。
|
|
9
|
+
|
|
10
|
+
2. 非商业授权
|
|
11
|
+
在遵守全部条款的前提下,权利人授予您全球范围、非独占、免许可费的权利,仅为非商业目的使用、复制、研究、修改和分发本软件的源码及编译版本。非商业目的指不主要旨在或用于获取商业利益、营业收入或金钱报酬的活动;个人非商业学习、无商业目的的教学、研究及公益使用可以适用。组织的营利或非营利身份本身不决定用途是否符合本条。
|
|
12
|
+
|
|
13
|
+
3. 商业用途限制
|
|
14
|
+
未经有关权利人另行明确书面授权,不得将本软件或其修改版本用于商业用途,包括但不限于:
|
|
15
|
+
(a) 销售、付费许可、出租本软件,或以其提供收费托管、SaaS、服务及付费产品;
|
|
16
|
+
(b) 集成到商业产品、收费分发包或以广告、订阅等方式获利的服务中;
|
|
17
|
+
(c) 在公司、组织或个人经营活动中,内部使用本软件开发、测试、维护或运营商业项目,即使本软件本身不对外分发或收费;
|
|
18
|
+
(d) 为客户提供有偿开发、咨询或其他商业交付时使用本软件。
|
|
19
|
+
免费提供、仅内部使用或修改名称,不构成商业用途豁免。用途不明确时,应先取得书面授权。
|
|
20
|
+
|
|
21
|
+
4. 复制、修改与分发条件
|
|
22
|
+
每份分发的源码或编译版本均须附带完整许可证及适用版权和第三方声明;不得删除或隐瞒这些声明。
|
|
23
|
+
修改版须清楚注明已修改、修改者及主要修改内容,不得冒充官方版本或暗示权利人背书。
|
|
24
|
+
您对本软件及基于本软件的修改部分的分发必须遵守本许可证,不得向接收者授予超出本许可证的商业使用权;接收者对原有部分的权利直接来自有关权利人。独立且非衍生的作品不因与本软件一同分发而自动适用本许可证。
|
|
25
|
+
本许可证不强制公开未分发的修改,也不要求分发编译版本时同时提供源码。所有分发仍须满足非商业条件。
|
|
26
|
+
|
|
27
|
+
5. 第三方材料与保留权利
|
|
28
|
+
依赖项及其他明确标注第三方许可的材料,仍各自适用其原许可证。本许可证不收回或限制第三方许可证已经授予的权利;仅独立使用这些材料时无需取得本项目的商业授权。
|
|
29
|
+
除明确授予的权利外,其余权利保留。本许可证不授予商标使用权,也不授予明示专利许可;不限制适用法律规定的不可排除权利。
|
|
30
|
+
|
|
31
|
+
6. 贡献与另行授权
|
|
32
|
+
贡献者保留其贡献的版权。主动向本项目提交并明确接受本许可证的贡献,按本许可证授权,不构成版权转让或额外商业再许可授权。维护者如为包含他人贡献的版本提供商业许可,须确保已取得所有必要权利;不得仅凭维护者身份授权不属于自己的权利。
|
|
33
|
+
商业授权请求可通过项目仓库联系维护者协商;沉默、公开源码或接受问题反馈不等于授权。
|
|
34
|
+
|
|
35
|
+
7. 终止
|
|
36
|
+
违反本许可证时,违约者依据本许可证取得的权利自动终止,应停止相关使用与分发。恢复授权须取得相关权利人的书面同意。终止不影响此前已合法取得副本并持续遵守本许可证的其他接收者,也不影响已经产生的责任。
|
|
37
|
+
|
|
38
|
+
8. 无担保与责任限制
|
|
39
|
+
在适用法律允许的最大范围内,本软件按“现状”提供,不提供任何明示或默示担保,包括适销性、特定用途适用性及不侵权担保。权利人和贡献者不对使用或无法使用本软件所致的损失、索赔或其他责任承担责任,无论基于合同、侵权或其他事由。法律不允许排除的担保或责任不受本条排除。
|
|
40
|
+
|
|
41
|
+
9. 其他
|
|
42
|
+
本许可证的任何条款不可执行时,其余条款在法律允许的范围内继续有效。后续发布的新许可版本不自动替换您依据本版本已合法取得副本所适用的条款。另行书面协议可对特定权利另作约定。
|
package/README.md
CHANGED
|
@@ -1,3 +1,77 @@
|
|
|
1
|
-
#
|
|
1
|
+
# 执行总线 / Execution Bus
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
DSH 独立插件 **@feng0531/dsh-execution-bus 0.1.1**,用于让模型提出一次执行请求后,由程序排队、等待和收集结果,而不是反复询问是否完成。
|
|
4
|
+
|
|
5
|
+
**当前是第一阶段可运行原型、部分完成,不是完整生产验收。只做插件,不修改 DSH。** 不增加模型或 Agent,不是工作流引擎、PTC 包装或任意命令执行服务,也不会改动其他插件。它不能解决模型理解错误、不断改变方案等所有推理问题。
|
|
6
|
+
|
|
7
|
+
## 已实现
|
|
8
|
+
|
|
9
|
+
- 版本化协议和运行时校验;有界 FIFO、并发限制、请求冲突检测、同请求执行复用。
|
|
10
|
+
- 原工具调用异步等待终态;复用宿主 Job 进度、取消和等待,无模型轮询。
|
|
11
|
+
- test:延迟、百分比、成功、失败、合作取消。
|
|
12
|
+
- host.read:出队时通过宿主工具注册表调用现有 read,保留当前身份、工作目录、权限及输出限制,不自行读文件或启动进程。
|
|
13
|
+
- 内存结果保存、大小限制、清理和关闭;失败日志不包含异常原文。
|
|
14
|
+
|
|
15
|
+
## 明确不支持 / 未验证
|
|
16
|
+
|
|
17
|
+
- 不持久化,不支持重启保留队列或恢复原工具调用,不自动重跑。
|
|
18
|
+
- 真实 AgentLoop 内存会话提交/继续已通过脚本适配器验证;GUI 实际展示、真实模型继续推理、磁盘会话提交与 PTC 完整链路未验证。
|
|
19
|
+
- 没有外部 Handler 传输、跨插件 Handler 服务、非幂等执行对账;不是全要求验收完成版。
|
|
20
|
+
|
|
21
|
+
详见 [宿主调用链与兼容性](docs/host-compatibility.md)、[协议和状态](docs/protocol.md)、[验证记录](docs/validation.md)。
|
|
22
|
+
|
|
23
|
+
## 构建与测试
|
|
24
|
+
|
|
25
|
+
需要 Node.js >=22(本次使用 24.21.0)和 npm。开发环境依赖安装限定本仓库,不触及日常 DSH。
|
|
26
|
+
|
|
27
|
+
```powershell
|
|
28
|
+
npm ci --ignore-scripts
|
|
29
|
+
npm test
|
|
30
|
+
npm run build
|
|
31
|
+
npm pack --ignore-scripts
|
|
32
|
+
npm run verify:package
|
|
33
|
+
npm run test:install
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
标签 CI、四矩阵测试与受控发布见 [发布流程](docs/RELEASING.md)。采用[非商业源码公开许可证](LICENSE),非 OSI 开源许可证;正式发布必须通过同标签 CI 和工作流门禁。发布不安装或重启 DSH。
|
|
37
|
+
|
|
38
|
+
测试会在隔离 Cordis 上下文运行真实宿主组件;Agent 身份为夹具,不调用真实模型。安装包只含编译入口、类型与文档,不包含宿主源码快照、测试环境或 node_modules。源码构建需要整个仓库。
|
|
39
|
+
|
|
40
|
+
## 安装:仅在你明确决定的隔离 DSH 配置中
|
|
41
|
+
|
|
42
|
+
本次**未安装到你的 DSH**。兼容目标精确为 DSH 0.2.0-rc.2 / Cordis 4.0.4;先确认你要操作的配置目录和宿主实际版本。不要向运行时安装目录复制文件或修改 ASAR。
|
|
43
|
+
|
|
44
|
+
1. 备份目标配置文件、依赖声明和 lockfile,记录当前插件清单。先停止目标隔离实例的工作,不动日常会话。
|
|
45
|
+
2. 在隔离配置的依赖目录安装本地 tgz,禁用生命周期脚本;避免升级宿主依赖。若 peer 不匹配,停止安装,不用 force/legacy-peer-deps 绕过。
|
|
46
|
+
3. 在目标实例已有的插件加载配置中加入包名。必须位于可见 tools、jobs、现有 tool-jobs controller 和 read 的组合内。包导出标准 Cordis name / inject / apply;不提供未验证的全局配置键。
|
|
47
|
+
4. 由使用者自行启动/重载隔离实例,确认 execution_bus 工具可见。配置结构随宿主组合而异,不承诺“一条配置适用于所有版本”。
|
|
48
|
+
|
|
49
|
+
## 使用
|
|
50
|
+
|
|
51
|
+
直接工具入口 execution_bus 的请求参数:
|
|
52
|
+
|
|
53
|
+
```json
|
|
54
|
+
{
|
|
55
|
+
"protocolVersion": 1,
|
|
56
|
+
"messageId": "message-001",
|
|
57
|
+
"requestId": "operation-001",
|
|
58
|
+
"type": "request",
|
|
59
|
+
"payload": {"handler": "test", "input": {"delayMs": 1000, "fail": false}}
|
|
60
|
+
}
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
调用会等待约一秒,再返回 succeeded 等终态,不需要 status 循环。真实只读任务把 payload 改为 {"handler":"host.read","input":{"file_path":"README.md","limit":20}},路径相对于可信宿主会话工作目录。
|
|
64
|
+
|
|
65
|
+
若当前 DSH 是 PTC 模式,在 run_code 内 await tools.execution_bus(同上对象),而非直接工具调用;这不会解除 run_code 的时间预算。PTC 端到端尚未验收。
|
|
66
|
+
|
|
67
|
+
手工查询/重取/取消:保留 requestId,换新 messageId,type 改为 status / result / cancel,payload 为 {}。不同会话无法读取或取消其他会话请求。cancelRequested 不等于已取消;读取执行状态字段判断。不要因为等待或通信超时就换 requestId 重新执行。
|
|
68
|
+
|
|
69
|
+
## 卸载与回滚
|
|
70
|
+
|
|
71
|
+
先停止新请求并等待已有操作终态,或请求合作取消。然后从目标隔离实例插件加载配置移除包名,按宿主正常生命周期卸载;卸载会取消并等待本插件受理的任务。不要强杀并假定外部操作已回滚。
|
|
72
|
+
|
|
73
|
+
若首次试用失败,恢复前述配置/依赖/锁文件备份,再由使用者自行启动目标实例。tgz 不写宿主文件、系统服务、全局命令或磁盘任务库,无专属磁盘数据迁移。进程关闭会丢失内存结果;宿主已有会话日志不在插件卸载范围内,不删除它们。
|
|
74
|
+
|
|
75
|
+
## 减少反复的验证
|
|
76
|
+
|
|
77
|
+
真实模型参与轮次、主动状态查询次数、重复执行次数和总耗时对照:**未验证**,不提供提升比例。自动化测试只证明等待和状态机制,不替代同条件真实模型对比。
|
package/dist/bus.d.ts
ADDED
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
export type State = "queued" | "running" | "succeeded" | "failed" | "cancelled" | "unknown";
|
|
2
|
+
export type Owner = {
|
|
3
|
+
sessionId: string;
|
|
4
|
+
callId: string;
|
|
5
|
+
};
|
|
6
|
+
export type Message = {
|
|
7
|
+
protocolVersion: 1;
|
|
8
|
+
messageId: string;
|
|
9
|
+
requestId: string;
|
|
10
|
+
type: "request" | "status" | "result" | "cancel";
|
|
11
|
+
payload: Record<string, unknown>;
|
|
12
|
+
};
|
|
13
|
+
export type Event = {
|
|
14
|
+
protocolVersion: 1;
|
|
15
|
+
messageId: string;
|
|
16
|
+
requestId: string;
|
|
17
|
+
correlationId: string;
|
|
18
|
+
sequence: number;
|
|
19
|
+
type: string;
|
|
20
|
+
payload: Record<string, unknown>;
|
|
21
|
+
};
|
|
22
|
+
export type Capability = (input: Record<string, unknown>, signal: AbortSignal) => Promise<unknown>;
|
|
23
|
+
export interface Handler {
|
|
24
|
+
validate(input: Record<string, unknown>): void;
|
|
25
|
+
run(input: Record<string, unknown>, ctx: {
|
|
26
|
+
signal: AbortSignal;
|
|
27
|
+
progress: (sequence: number, percent: number) => void;
|
|
28
|
+
requestId: string;
|
|
29
|
+
invoke?: Capability;
|
|
30
|
+
}): Promise<unknown>;
|
|
31
|
+
}
|
|
32
|
+
export interface RecordView {
|
|
33
|
+
requestId: string;
|
|
34
|
+
state: State;
|
|
35
|
+
sequence: number;
|
|
36
|
+
cancelRequested: boolean;
|
|
37
|
+
delivery: "pending" | "returned";
|
|
38
|
+
result?: unknown;
|
|
39
|
+
error?: string;
|
|
40
|
+
progress?: number;
|
|
41
|
+
}
|
|
42
|
+
export declare function object(v: unknown): Record<string, unknown>;
|
|
43
|
+
export declare function parse(raw: unknown): Message;
|
|
44
|
+
/** Volatile single-process bus; no acceptance durability or automatic execution retry. */
|
|
45
|
+
export declare class ExecutionBus {
|
|
46
|
+
readonly options: {
|
|
47
|
+
concurrency: number;
|
|
48
|
+
queueLimit: number;
|
|
49
|
+
recordLimit: number;
|
|
50
|
+
resultBytes: number;
|
|
51
|
+
retentionMs: number;
|
|
52
|
+
};
|
|
53
|
+
private entries;
|
|
54
|
+
private handlers;
|
|
55
|
+
private queue;
|
|
56
|
+
private running;
|
|
57
|
+
private closed;
|
|
58
|
+
constructor(options?: {
|
|
59
|
+
concurrency: number;
|
|
60
|
+
queueLimit: number;
|
|
61
|
+
recordLimit: number;
|
|
62
|
+
resultBytes: number;
|
|
63
|
+
retentionMs: number;
|
|
64
|
+
});
|
|
65
|
+
register(name: string, handler: Handler): () => void;
|
|
66
|
+
private key;
|
|
67
|
+
private emit;
|
|
68
|
+
private view;
|
|
69
|
+
private get;
|
|
70
|
+
sweep(now?: number): void;
|
|
71
|
+
submit(owner: Owner, raw: unknown, onEvent?: (e: Event) => void, invoke?: Capability): {
|
|
72
|
+
accepted: Event;
|
|
73
|
+
done: Promise<RecordView>;
|
|
74
|
+
cancel: () => void;
|
|
75
|
+
detach: () => void;
|
|
76
|
+
returned: () => void;
|
|
77
|
+
};
|
|
78
|
+
private attachment;
|
|
79
|
+
command(owner: Owner, raw: unknown): RecordView;
|
|
80
|
+
private cancel;
|
|
81
|
+
private finish;
|
|
82
|
+
private pump;
|
|
83
|
+
private run;
|
|
84
|
+
close(): Promise<void>;
|
|
85
|
+
}
|
package/dist/bus.js
ADDED
|
@@ -0,0 +1,328 @@
|
|
|
1
|
+
import { randomUUID, createHash } from "node:crypto";
|
|
2
|
+
export function object(v) {
|
|
3
|
+
if (!v || typeof v !== "object" || Array.isArray(v))
|
|
4
|
+
throw Error("INVALID_OBJECT");
|
|
5
|
+
return v;
|
|
6
|
+
}
|
|
7
|
+
function canonical(v) {
|
|
8
|
+
if (v === null || typeof v !== "object")
|
|
9
|
+
return JSON.stringify(v);
|
|
10
|
+
if (Array.isArray(v))
|
|
11
|
+
return "[" + v.map(canonical).join(",") + "]";
|
|
12
|
+
return ("{" +
|
|
13
|
+
Object.keys(v)
|
|
14
|
+
.sort()
|
|
15
|
+
.map((k) => JSON.stringify(k) +
|
|
16
|
+
":" +
|
|
17
|
+
canonical(v[k]))
|
|
18
|
+
.join(",") +
|
|
19
|
+
"}");
|
|
20
|
+
}
|
|
21
|
+
export function parse(raw) {
|
|
22
|
+
const m = object(raw);
|
|
23
|
+
if (Object.keys(m).some((k) => ![
|
|
24
|
+
"protocolVersion",
|
|
25
|
+
"messageId",
|
|
26
|
+
"requestId",
|
|
27
|
+
"type",
|
|
28
|
+
"payload",
|
|
29
|
+
].includes(k)))
|
|
30
|
+
throw Error("UNKNOWN_FIELD");
|
|
31
|
+
if (m.protocolVersion !== 1)
|
|
32
|
+
throw Error("UNSUPPORTED_VERSION");
|
|
33
|
+
for (const k of ["messageId", "requestId"])
|
|
34
|
+
if (typeof m[k] !== "string" ||
|
|
35
|
+
!/^[a-zA-Z0-9_.:-]{1,128}$/.test(m[k]))
|
|
36
|
+
throw Error("INVALID_ID");
|
|
37
|
+
if (!["request", "status", "result", "cancel"].includes(m.type))
|
|
38
|
+
throw Error("UNSUPPORTED_TYPE");
|
|
39
|
+
object(m.payload);
|
|
40
|
+
if (Buffer.byteLength(JSON.stringify(m)) > 16384)
|
|
41
|
+
throw Error("MESSAGE_TOO_LARGE");
|
|
42
|
+
return structuredClone(m);
|
|
43
|
+
}
|
|
44
|
+
function freezeJson(value) {
|
|
45
|
+
if (value && typeof value === "object") {
|
|
46
|
+
for (const child of Object.values(value))
|
|
47
|
+
freezeJson(child);
|
|
48
|
+
Object.freeze(value);
|
|
49
|
+
}
|
|
50
|
+
return value;
|
|
51
|
+
}
|
|
52
|
+
const terminal = (s) => !["queued", "running"].includes(s);
|
|
53
|
+
/** Volatile single-process bus; no acceptance durability or automatic execution retry. */
|
|
54
|
+
export class ExecutionBus {
|
|
55
|
+
options;
|
|
56
|
+
entries = new Map();
|
|
57
|
+
handlers = new Map();
|
|
58
|
+
queue = [];
|
|
59
|
+
running = 0;
|
|
60
|
+
closed = false;
|
|
61
|
+
constructor(options = {
|
|
62
|
+
concurrency: 2,
|
|
63
|
+
queueLimit: 32,
|
|
64
|
+
recordLimit: 256,
|
|
65
|
+
resultBytes: 65536,
|
|
66
|
+
retentionMs: 3600000,
|
|
67
|
+
}) {
|
|
68
|
+
this.options = options;
|
|
69
|
+
for (const v of Object.values(options))
|
|
70
|
+
if (!Number.isSafeInteger(v) || v < 1)
|
|
71
|
+
throw Error("INVALID_OPTIONS");
|
|
72
|
+
}
|
|
73
|
+
register(name, handler) {
|
|
74
|
+
if (this.handlers.has(name))
|
|
75
|
+
throw Error("DUPLICATE_HANDLER");
|
|
76
|
+
this.handlers.set(name, handler);
|
|
77
|
+
return () => {
|
|
78
|
+
this.handlers.delete(name);
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
key(o, id) {
|
|
82
|
+
if (!o.sessionId || !o.callId)
|
|
83
|
+
throw Error("UNTRUSTED_CONTEXT");
|
|
84
|
+
return JSON.stringify([o.sessionId, id]);
|
|
85
|
+
}
|
|
86
|
+
emit(e, type, payload, correlationId = "") {
|
|
87
|
+
e.view.sequence++;
|
|
88
|
+
const event = {
|
|
89
|
+
protocolVersion: 1,
|
|
90
|
+
messageId: randomUUID(),
|
|
91
|
+
requestId: e.view.requestId,
|
|
92
|
+
correlationId: correlationId || e.correlationId,
|
|
93
|
+
sequence: e.view.sequence,
|
|
94
|
+
type,
|
|
95
|
+
payload,
|
|
96
|
+
};
|
|
97
|
+
for (const f of e.listeners)
|
|
98
|
+
try {
|
|
99
|
+
f(event);
|
|
100
|
+
}
|
|
101
|
+
catch {
|
|
102
|
+
/* Observers cannot alter execution. */
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
view(e) {
|
|
106
|
+
return structuredClone(e.view);
|
|
107
|
+
}
|
|
108
|
+
get(o, id) {
|
|
109
|
+
const e = this.entries.get(this.key(o, id));
|
|
110
|
+
if (!e)
|
|
111
|
+
throw Error("NOT_FOUND");
|
|
112
|
+
return e;
|
|
113
|
+
}
|
|
114
|
+
sweep(now = Date.now()) {
|
|
115
|
+
for (const [k, e] of this.entries)
|
|
116
|
+
if (terminal(e.view.state) && now - e.created > this.options.retentionMs)
|
|
117
|
+
this.entries.delete(k);
|
|
118
|
+
}
|
|
119
|
+
submit(owner, raw, onEvent, invoke) {
|
|
120
|
+
if (this.closed)
|
|
121
|
+
throw Error("CLOSED");
|
|
122
|
+
const m = parse(raw);
|
|
123
|
+
if (m.type !== "request")
|
|
124
|
+
throw Error("EXPECTED_REQUEST");
|
|
125
|
+
const p = m.payload;
|
|
126
|
+
if (Object.keys(p).some((k) => !["handler", "input"].includes(k)) ||
|
|
127
|
+
typeof p.handler !== "string")
|
|
128
|
+
throw Error("INVALID_REQUEST");
|
|
129
|
+
const input = object(p.input);
|
|
130
|
+
const key = this.key(owner, m.requestId), fp = createHash("sha256").update(canonical(p)).digest("hex");
|
|
131
|
+
this.sweep();
|
|
132
|
+
let e = this.entries.get(key);
|
|
133
|
+
if (e) {
|
|
134
|
+
if (e.fingerprint !== fp)
|
|
135
|
+
throw Error("REQUEST_CONFLICT");
|
|
136
|
+
const previous = e.messages.get(m.messageId);
|
|
137
|
+
if (previous && previous !== canonical(m))
|
|
138
|
+
throw Error("MESSAGE_CONFLICT");
|
|
139
|
+
if (!previous) {
|
|
140
|
+
if (e.messages.size >= 128)
|
|
141
|
+
throw Error("MESSAGE_LIMIT");
|
|
142
|
+
e.messages.set(m.messageId, canonical(m));
|
|
143
|
+
}
|
|
144
|
+
return this.attachment(e, onEvent, m.messageId);
|
|
145
|
+
}
|
|
146
|
+
const handler = this.handlers.get(p.handler);
|
|
147
|
+
if (!handler)
|
|
148
|
+
throw Error("UNKNOWN_HANDLER");
|
|
149
|
+
handler.validate(input);
|
|
150
|
+
if (this.entries.size >= this.options.recordLimit ||
|
|
151
|
+
this.queue.length >= this.options.queueLimit)
|
|
152
|
+
throw Error("BACKPRESSURE");
|
|
153
|
+
let resolve;
|
|
154
|
+
const done = new Promise((r) => (resolve = r));
|
|
155
|
+
e = {
|
|
156
|
+
invoke,
|
|
157
|
+
correlationId: m.messageId,
|
|
158
|
+
owner: owner.sessionId,
|
|
159
|
+
fingerprint: fp,
|
|
160
|
+
handler,
|
|
161
|
+
input: structuredClone(input),
|
|
162
|
+
view: {
|
|
163
|
+
requestId: m.requestId,
|
|
164
|
+
state: "queued",
|
|
165
|
+
sequence: 0,
|
|
166
|
+
cancelRequested: false,
|
|
167
|
+
delivery: "pending",
|
|
168
|
+
},
|
|
169
|
+
controller: new AbortController(),
|
|
170
|
+
done,
|
|
171
|
+
resolve,
|
|
172
|
+
listeners: new Set(),
|
|
173
|
+
sourceSequence: 0,
|
|
174
|
+
created: Date.now(),
|
|
175
|
+
messages: new Map([[m.messageId, canonical(m)]]),
|
|
176
|
+
};
|
|
177
|
+
this.entries.set(key, e);
|
|
178
|
+
const attached = this.attachment(e, onEvent, m.messageId);
|
|
179
|
+
this.queue.push(e);
|
|
180
|
+
queueMicrotask(() => this.pump());
|
|
181
|
+
return attached;
|
|
182
|
+
}
|
|
183
|
+
attachment(e, onEvent, id) {
|
|
184
|
+
if (onEvent && !terminal(e.view.state)) {
|
|
185
|
+
if (e.listeners.size >= 32)
|
|
186
|
+
throw Error("WAITER_LIMIT");
|
|
187
|
+
e.listeners.add(onEvent);
|
|
188
|
+
}
|
|
189
|
+
const accepted = {
|
|
190
|
+
protocolVersion: 1,
|
|
191
|
+
messageId: randomUUID(),
|
|
192
|
+
requestId: e.view.requestId,
|
|
193
|
+
correlationId: id,
|
|
194
|
+
sequence: e.view.sequence,
|
|
195
|
+
type: "accepted",
|
|
196
|
+
payload: { state: e.view.state, durable: false },
|
|
197
|
+
};
|
|
198
|
+
return {
|
|
199
|
+
accepted,
|
|
200
|
+
done: e.done,
|
|
201
|
+
cancel: () => this.cancel(e),
|
|
202
|
+
detach: () => {
|
|
203
|
+
if (onEvent)
|
|
204
|
+
e.listeners.delete(onEvent);
|
|
205
|
+
},
|
|
206
|
+
returned: () => {
|
|
207
|
+
e.view.delivery = "returned";
|
|
208
|
+
},
|
|
209
|
+
};
|
|
210
|
+
}
|
|
211
|
+
command(owner, raw) {
|
|
212
|
+
const m = parse(raw);
|
|
213
|
+
if (m.type === "request")
|
|
214
|
+
throw Error("EXPECTED_CONTROL");
|
|
215
|
+
if (Object.keys(m.payload).length)
|
|
216
|
+
throw Error("INVALID_CONTROL");
|
|
217
|
+
this.sweep();
|
|
218
|
+
const e = this.get(owner, m.requestId);
|
|
219
|
+
const previous = e.messages.get(m.messageId), fingerprint = canonical(m);
|
|
220
|
+
if (previous && previous !== fingerprint)
|
|
221
|
+
throw Error("MESSAGE_CONFLICT");
|
|
222
|
+
if (!previous) {
|
|
223
|
+
if (e.messages.size >= 128)
|
|
224
|
+
throw Error("MESSAGE_LIMIT");
|
|
225
|
+
e.messages.set(m.messageId, fingerprint);
|
|
226
|
+
if (m.type === "cancel")
|
|
227
|
+
this.cancel(e);
|
|
228
|
+
}
|
|
229
|
+
return this.view(e);
|
|
230
|
+
}
|
|
231
|
+
cancel(e) {
|
|
232
|
+
if (terminal(e.view.state) || e.view.cancelRequested)
|
|
233
|
+
return;
|
|
234
|
+
e.view.cancelRequested = true;
|
|
235
|
+
e.controller.abort();
|
|
236
|
+
this.emit(e, "cancel_requested", {});
|
|
237
|
+
if (e.view.state === "queued") {
|
|
238
|
+
this.queue = this.queue.filter((x) => x !== e);
|
|
239
|
+
this.finish(e, "cancelled");
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
finish(e, state, result, error) {
|
|
243
|
+
if (terminal(e.view.state))
|
|
244
|
+
return;
|
|
245
|
+
e.created = Date.now();
|
|
246
|
+
e.view.state = state;
|
|
247
|
+
if (result !== undefined)
|
|
248
|
+
e.view.result = result;
|
|
249
|
+
if (error)
|
|
250
|
+
e.view.error = error;
|
|
251
|
+
this.emit(e, state, { state });
|
|
252
|
+
e.resolve(freezeJson(this.view(e)));
|
|
253
|
+
e.listeners.clear();
|
|
254
|
+
}
|
|
255
|
+
pump() {
|
|
256
|
+
while (!this.closed &&
|
|
257
|
+
this.running < this.options.concurrency &&
|
|
258
|
+
this.queue.length) {
|
|
259
|
+
const e = this.queue.shift();
|
|
260
|
+
if (e.view.state !== "queued")
|
|
261
|
+
continue;
|
|
262
|
+
this.running++;
|
|
263
|
+
e.view.state = "running";
|
|
264
|
+
this.emit(e, "started", {});
|
|
265
|
+
void this.run(e).finally(() => {
|
|
266
|
+
this.running--;
|
|
267
|
+
this.pump();
|
|
268
|
+
});
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
async run(e) {
|
|
272
|
+
try {
|
|
273
|
+
const result = await e.handler.run(e.input, {
|
|
274
|
+
invoke: e.invoke,
|
|
275
|
+
signal: e.controller.signal,
|
|
276
|
+
requestId: e.view.requestId,
|
|
277
|
+
progress: (seq, pct) => {
|
|
278
|
+
if (e.view.state !== "running" ||
|
|
279
|
+
!Number.isSafeInteger(seq) ||
|
|
280
|
+
seq <= e.sourceSequence ||
|
|
281
|
+
!Number.isFinite(pct) ||
|
|
282
|
+
pct < 0 ||
|
|
283
|
+
pct > 100)
|
|
284
|
+
return;
|
|
285
|
+
e.sourceSequence = seq;
|
|
286
|
+
e.view.progress = pct;
|
|
287
|
+
this.emit(e, "progress", { percent: pct });
|
|
288
|
+
},
|
|
289
|
+
});
|
|
290
|
+
let json;
|
|
291
|
+
try {
|
|
292
|
+
json = JSON.stringify(result);
|
|
293
|
+
}
|
|
294
|
+
catch {
|
|
295
|
+
this.finish(e, "unknown", undefined, "RESULT_NOT_RETAINED");
|
|
296
|
+
return;
|
|
297
|
+
}
|
|
298
|
+
if (json === undefined ||
|
|
299
|
+
Buffer.byteLength(json) > this.options.resultBytes) {
|
|
300
|
+
this.finish(e, "unknown", undefined, "RESULT_NOT_RETAINED");
|
|
301
|
+
return;
|
|
302
|
+
}
|
|
303
|
+
this.finish(e, "succeeded", JSON.parse(json));
|
|
304
|
+
}
|
|
305
|
+
catch (err) {
|
|
306
|
+
const cancelled = e.controller.signal.aborted &&
|
|
307
|
+
err instanceof Error &&
|
|
308
|
+
err.name === "AbortError";
|
|
309
|
+
this.finish(e, cancelled
|
|
310
|
+
? "cancelled"
|
|
311
|
+
: e.controller.signal.aborted
|
|
312
|
+
? "unknown"
|
|
313
|
+
: "failed", undefined, cancelled
|
|
314
|
+
? "CANCELLED"
|
|
315
|
+
: e.controller.signal.aborted
|
|
316
|
+
? "CANCELLATION_UNCONFIRMED"
|
|
317
|
+
: "HANDLER_FAILED");
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
async close() {
|
|
321
|
+
this.closed = true;
|
|
322
|
+
for (const e of this.entries.values())
|
|
323
|
+
this.cancel(e);
|
|
324
|
+
await Promise.all([...this.entries.values()].map((e) => e.done));
|
|
325
|
+
this.handlers.clear();
|
|
326
|
+
this.entries.clear();
|
|
327
|
+
}
|
|
328
|
+
}
|
package/dist/handlers.js
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { setTimeout } from "node:timers/promises";
|
|
2
|
+
export const testHandler = {
|
|
3
|
+
validate(input) {
|
|
4
|
+
if (Object.keys(input).some((k) => !["delayMs", "fail"].includes(k)) ||
|
|
5
|
+
!Number.isSafeInteger(input.delayMs) ||
|
|
6
|
+
Number(input.delayMs) < 0 ||
|
|
7
|
+
Number(input.delayMs) > 30000 ||
|
|
8
|
+
typeof input.fail !== "boolean")
|
|
9
|
+
throw Error("INVALID_TEST_INPUT");
|
|
10
|
+
},
|
|
11
|
+
async run(input, ctx) {
|
|
12
|
+
for (let i = 1; i <= 5; i++) {
|
|
13
|
+
await setTimeout(Number(input.delayMs) / 5, undefined, {
|
|
14
|
+
signal: ctx.signal,
|
|
15
|
+
});
|
|
16
|
+
ctx.progress(i, i * 20);
|
|
17
|
+
}
|
|
18
|
+
if (input.fail)
|
|
19
|
+
throw Error("CONTROLLED_FAILURE");
|
|
20
|
+
return { ok: true };
|
|
21
|
+
},
|
|
22
|
+
};
|
|
23
|
+
export const readHandler = {
|
|
24
|
+
validate(input) {
|
|
25
|
+
if (Object.keys(input).some((k) => !["file_path", "offset", "limit"].includes(k)) ||
|
|
26
|
+
typeof input.file_path !== "string" ||
|
|
27
|
+
!input.file_path ||
|
|
28
|
+
input.file_path.length > 2048)
|
|
29
|
+
throw Error("INVALID_READ_INPUT");
|
|
30
|
+
for (const k of ["offset", "limit"])
|
|
31
|
+
if (input[k] !== undefined &&
|
|
32
|
+
(!Number.isSafeInteger(input[k]) || Number(input[k]) < 1))
|
|
33
|
+
throw Error("INVALID_READ_INPUT");
|
|
34
|
+
if (Number(input.limit ?? 100) > 100)
|
|
35
|
+
throw Error("READ_LIMIT");
|
|
36
|
+
},
|
|
37
|
+
async run(input, ctx) {
|
|
38
|
+
if (!ctx.invoke)
|
|
39
|
+
throw Error("HOST_CAPABILITY_REQUIRED");
|
|
40
|
+
return ctx.invoke({ ...input, limit: input.limit ?? 100 }, ctx.signal);
|
|
41
|
+
},
|
|
42
|
+
};
|
package/dist/plugin.d.ts
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { Context } from "@deepseek-ai/cordis";
|
|
2
|
+
declare module "@deepseek-ai/dsh-jobs" {
|
|
3
|
+
interface JobKindMap {
|
|
4
|
+
executionBus: "execution-bus";
|
|
5
|
+
}
|
|
6
|
+
}
|
|
7
|
+
export declare const name = "execution-bus";
|
|
8
|
+
export declare const inject: string[];
|
|
9
|
+
export declare function apply(ctx: Context): void;
|
package/dist/plugin.js
ADDED
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
import { ExecutionBus, parse } from "./bus.js";
|
|
3
|
+
import { testHandler, readHandler } from "./handlers.js";
|
|
4
|
+
export const name = "execution-bus";
|
|
5
|
+
export const inject = ["tools", "jobs"];
|
|
6
|
+
export function apply(ctx) {
|
|
7
|
+
const bus = new ExecutionBus();
|
|
8
|
+
bus.register("test", testHandler);
|
|
9
|
+
bus.register("host.read", readHandler);
|
|
10
|
+
const sharedJobs = new Map();
|
|
11
|
+
ctx.effect(() => async () => {
|
|
12
|
+
await bus.close();
|
|
13
|
+
await Promise.allSettled(sharedJobs.values());
|
|
14
|
+
sharedJobs.clear();
|
|
15
|
+
});
|
|
16
|
+
ctx.tools.register({
|
|
17
|
+
name: "execution_bus",
|
|
18
|
+
description: "执行总线 / Execution Bus. Submit a version 1 request and WAIT for its terminal result in this tool call. Reuse requestId only for the same logical operation. Handlers: test {delayMs:0..30000,fail:boolean}, host.read {file_path,offset?,limit?<=100}. Control types status/result/cancel use empty payload. Acceptance is volatile, not execution success. No polling needed; no restart resume.",
|
|
19
|
+
parameters: {
|
|
20
|
+
type: "object",
|
|
21
|
+
additionalProperties: false,
|
|
22
|
+
required: [
|
|
23
|
+
"protocolVersion",
|
|
24
|
+
"messageId",
|
|
25
|
+
"requestId",
|
|
26
|
+
"type",
|
|
27
|
+
"payload",
|
|
28
|
+
],
|
|
29
|
+
properties: {
|
|
30
|
+
protocolVersion: { type: "integer", const: 1 },
|
|
31
|
+
messageId: { type: "string" },
|
|
32
|
+
requestId: { type: "string" },
|
|
33
|
+
type: {
|
|
34
|
+
type: "string",
|
|
35
|
+
enum: ["request", "status", "result", "cancel"],
|
|
36
|
+
},
|
|
37
|
+
payload: { type: "object" },
|
|
38
|
+
},
|
|
39
|
+
},
|
|
40
|
+
output: {
|
|
41
|
+
schema: {},
|
|
42
|
+
render: (_a, v) => [{ type: "text", text: JSON.stringify(v) }],
|
|
43
|
+
},
|
|
44
|
+
async execute(raw, exec) {
|
|
45
|
+
if (!exec.agent)
|
|
46
|
+
throw Error("OWNING_SESSION_REQUIRED");
|
|
47
|
+
const owner = {
|
|
48
|
+
sessionId: String(exec.agent.id),
|
|
49
|
+
callId: String(exec.callId),
|
|
50
|
+
}, m = parse(raw);
|
|
51
|
+
if (m.type !== "request")
|
|
52
|
+
return bus.command(owner, m);
|
|
53
|
+
let handle;
|
|
54
|
+
const attached = bus.submit(owner, m, (event) => handle?.updateProgress(event.type === "progress"
|
|
55
|
+
? String(event.payload.percent) + "%"
|
|
56
|
+
: event.type), async (input, signal) => {
|
|
57
|
+
const result = await ctx.tools.execute({
|
|
58
|
+
name: "read",
|
|
59
|
+
callId: (String(exec.callId) +
|
|
60
|
+
":bus:" +
|
|
61
|
+
randomUUID()),
|
|
62
|
+
rootCallId: exec.rootCallId,
|
|
63
|
+
parent: exec.token,
|
|
64
|
+
agent: exec.agent,
|
|
65
|
+
arguments: input,
|
|
66
|
+
signal,
|
|
67
|
+
});
|
|
68
|
+
if (result.isError)
|
|
69
|
+
throw Error("HOST_READ_FAILED");
|
|
70
|
+
return result.value;
|
|
71
|
+
});
|
|
72
|
+
const abort = () => attached.cancel();
|
|
73
|
+
const jobKey = JSON.stringify([owner.sessionId, m.requestId]);
|
|
74
|
+
let settled = sharedJobs.get(jobKey);
|
|
75
|
+
if (!settled) {
|
|
76
|
+
try {
|
|
77
|
+
const job = ctx.jobs.start({
|
|
78
|
+
kind: "execution-bus",
|
|
79
|
+
label: "执行总线 / Execution Bus",
|
|
80
|
+
owner: exec.agent.id,
|
|
81
|
+
run(h) {
|
|
82
|
+
handle = h;
|
|
83
|
+
return {
|
|
84
|
+
done: attached.done.then((v) => ({
|
|
85
|
+
status: v.state === "succeeded"
|
|
86
|
+
? "completed"
|
|
87
|
+
: v.state === "cancelled"
|
|
88
|
+
? "killed"
|
|
89
|
+
: "failed",
|
|
90
|
+
detail: v.state,
|
|
91
|
+
})),
|
|
92
|
+
cancel: abort,
|
|
93
|
+
};
|
|
94
|
+
},
|
|
95
|
+
});
|
|
96
|
+
const ownerId = exec.agent.id;
|
|
97
|
+
// One registered host waiter suppresses extra completion messages for every attachment.
|
|
98
|
+
settled = (async () => {
|
|
99
|
+
for (;;) {
|
|
100
|
+
const view = await ctx.jobs.wait(job, 60000, ownerId);
|
|
101
|
+
if (view.status !== "running" && view.status !== "stopping")
|
|
102
|
+
break;
|
|
103
|
+
}
|
|
104
|
+
ctx.jobs.remove(job, ownerId);
|
|
105
|
+
})();
|
|
106
|
+
sharedJobs.set(jobKey, settled);
|
|
107
|
+
void settled.then(() => sharedJobs.delete(jobKey), () => sharedJobs.delete(jobKey));
|
|
108
|
+
}
|
|
109
|
+
catch {
|
|
110
|
+
abort();
|
|
111
|
+
await attached.done;
|
|
112
|
+
attached.detach();
|
|
113
|
+
throw Error("HOST_JOB_UNAVAILABLE");
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
exec.signal.addEventListener("abort", abort, { once: true });
|
|
117
|
+
if (exec.signal.aborted)
|
|
118
|
+
abort();
|
|
119
|
+
try {
|
|
120
|
+
await settled;
|
|
121
|
+
const value = await attached.done;
|
|
122
|
+
attached.returned();
|
|
123
|
+
return value;
|
|
124
|
+
}
|
|
125
|
+
finally {
|
|
126
|
+
exec.signal.removeEventListener("abort", abort);
|
|
127
|
+
attached.detach();
|
|
128
|
+
}
|
|
129
|
+
},
|
|
130
|
+
});
|
|
131
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
# 不安装插件的流程测试
|
|
2
|
+
|
|
3
|
+
## 本次执行方式与边界
|
|
4
|
+
|
|
5
|
+
当前会话没有 execution_bus 工具,桌面配置未安装本插件。本次由当前会话运行测试命令,在独立进程中加载本仓库编译产物及真实 DSH npm 组件;不修改日常配置、不热注入运行宿主、不重启 DSH。
|
|
6
|
+
|
|
7
|
+
这能验证真实 AgentLoop、SessionStore、ToolRuntime、Job 与 read 组件路径;不等于本会话实际调用 execution_bus,不验证真实桌面 UI、PTC 进程或真实模型。真实 AgentLoop 测试使用脚本模型适配器,read 组件测试使用测试 Agent 身份。两条测试不能合称当前会话端到端通过。
|
|
8
|
+
|
|
9
|
+
## 重复运行
|
|
10
|
+
|
|
11
|
+
1. npm ci --include=dev --ignore-scripts
|
|
12
|
+
2. npm run test:flow:流程回归、真实宿主执行组件、真实 AgentLoop 原调用结果关联。
|
|
13
|
+
3. npm run check:包含核心队列、安全和发布门禁的全部测试。
|
|
14
|
+
4. npm run verify:package:包白名单。
|
|
15
|
+
|
|
16
|
+
流程包括:正常成功/失败、读文件、权限拒绝、匿名请求拒绝、重复请求只新增一个 Job、工具取消、插件关闭、进度不作为聊天消息、原调用只提交一次后才继续下一模型适配器请求。
|
|
17
|
+
|
|
18
|
+
## 本次可复现问题
|
|
19
|
+
|
|
20
|
+
| 问题 | 修复前证据 | 本地修复 | 影响范围 |
|
|
21
|
+
|---|---|---|---|
|
|
22
|
+
| core 返回的 done 结果对象共享且可修改 | 第一个消费者将 state/result 改写后,第二次同请求取得被修改值 | 深度冻结终态快照 | core 扩展接入;不能据此声称模型能修改宿主冻结结果 |
|
|
23
|
+
| Handler 完成但结果 JSON.stringify 抛错被记为 failed | 返回 BigInt 的一次执行被误记 failed | 保存异常为 unknown / RESULT_NOT_RETAINED,不重跑 | 当前内置只读/测试返回值正常;扩展 Handler 有风险 |
|
|
24
|
+
| 结果超期查询未检查 TTL | 无新 request 时,过期 result 仍可读 | command 查询前 sweep | 查询/重取/取消均遵守结果保存期 |
|
|
25
|
+
|
|
26
|
+
三个测试在修复前均失败;修复后全量 24/24 通过。这些修复归入 0.1.1 补丁发布;不改变已有 0.1.0 标签或公开包。发布状态以 GitHub Release 和 npm 公开结果为准。
|
|
27
|
+
|
|
28
|
+
## 优化建议
|
|
29
|
+
|
|
30
|
+
1. 已优化:终态结果不可变;保存失败明确区分执行失败;查询触发 TTL 回收,无常驻清理定时器。
|
|
31
|
+
2. 后续可优化:完成请求重取无需新建短暂 Job;现在不会重复执行,但会产生不必要的 UI 生命周期事件。
|
|
32
|
+
3. 后续可优化:每个重复附着仍注册一个空进度监听器;可将进度观察者绑定到共享逻辑请求,避免消耗 32 个监听者限额。
|
|
33
|
+
4. 后续可优化:宿主 read 取消错误仍统一映射 HOST_READ_FAILED,保守归为 unknown。应保留经过白名单验证的取消类型,而不泄露原始异常文本。
|
|
34
|
+
5. 不建议盲目改动:取消一个附着工具会请求取消整个逻辑执行,是当前明确语义;改成仅取消等待者需要单独设计宿主 drain、Job 通知和结果留存,不能只 Promise.race 后抛弃原执行。
|
|
35
|
+
|
|
36
|
+
## 真实当前会话测试前提
|
|
37
|
+
|
|
38
|
+
只有宿主通过正常插件加载向当前 Agent 注册工具后,才可能真正执行 await tools.execution_bus。当前没有暴露可用的临时插件注册工具,不能用额外 Node 进程、伪造 sessionId 或追加聊天结果来替代。若后续要测试真实当前会话,需要另行授权安装/加载并确认工具可见;本次未执行安装。
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# 测试与发布流程
|
|
2
|
+
|
|
3
|
+
参考 dsh-vcs 的标签 CI 与联合发布流程,针对纯后端执行总线适配。没有复制 SVN、Monaco 或浏览器 layout 检查,因为本项目没有这些依赖和前端。
|
|
4
|
+
|
|
5
|
+
## 触发策略
|
|
6
|
+
|
|
7
|
+
普通分支 push 和 pull_request 不触发 CI。仅 vX.Y.Z 标签 push 和手动 workflow_dispatch 触发 CI;Windows/Ubuntu × Node 22/24 四项矩阵运行相同的构建、测试、包白名单、候选打包和生产依赖隔离安装。手动分支 CI 不能替代正式发布所需的同标签 push CI。
|
|
8
|
+
|
|
9
|
+
## 本地验证
|
|
10
|
+
|
|
11
|
+
依次运行 npm ci --include=dev --ignore-scripts、npm run check、npm run verify:package、npm run pack:candidate、npm run test:install。test:install 默认新打包,也允许传入一个本地 tgz。安装目录在系统临时目录中,结束后清理,不操作用户 DSH 配置。
|
|
12
|
+
|
|
13
|
+
每个版本必须有独立中文发布说明 docs/releases/X.Y.Z.md,注明实际模型/桌面/跨重启等未验证项。package.json、package-lock.json 根版本及标签必须一致。不移动已有标签。
|
|
14
|
+
|
|
15
|
+
## 发布门禁
|
|
16
|
+
|
|
17
|
+
用户已授权 0.1.0 正式发布,并确认沿用 dsh-vcs 的非商业源码公开许可证。该许可不是 OSI 开源许可证,限制商业项目内部使用与有偿交付,具体以 LICENSE 为准。用户已持续授权本项目每轮改动完成并验证通过后主动正式发布,无需逐次提醒;仍必须通过工作流确认与完整门禁,不表示实际模型验收通过。版本采用 vX.Y.Z:X 大版本、Y 小版本/新增功能、Z BUG 修复;发布不自动安装或重启 DSH。
|
|
18
|
+
|
|
19
|
+
准备正式发布前:由用户明确授权;确定 LICENSE 与包 license 字段;检查公开材料、第三方许可和敏感信息;更新版本、锁文件与发布说明;提交并推送已批准的版本标签;等待四矩阵通过。
|
|
20
|
+
|
|
21
|
+
从 main 手动运行 Publish GitHub and npm,输入已存在标签及确认词 publish-execution-bus。推荐为 npm-release environment 配置必须审批的维护者及适当分支限制,并配置 NPM_AUTOMATION_TOKEN。仓库环境审批和 token 均需要维护者配置,本次没有配置远端。
|
|
22
|
+
|
|
23
|
+
工作流重新检查标签版本与许可证,运行本地检查,生成候选,查询该精确标签/提交的最新 push CI 为 success,下载四个矩阵候选并逐字节核对。然后隔离安装同一候选、保留制品,仅 npm 上传步骤取得凭据。npm 公开元数据 integrity 与实际下载字节必须一致,才创建 GitHub Release;附件是同一个 tgz 与 SHA256SUMS.txt。不自动覆盖已有 Release。
|
|
24
|
+
|
|
25
|
+
npm 与 GitHub 不是原子发布。如果上传后公开包 404,不盲目重传;先只读对账,再重跑原工作流。已有 npm 同版本必须 integrity 相同才跳过上传;不匹配立即停止。发布过程没有自动重试上传,公开可见性最多查询十次。
|
|
26
|
+
|
|
27
|
+
## 验证边界
|
|
28
|
+
|
|
29
|
+
本地 Windows/Node 24 的脚本验证不等于 GitHub 四平台已成功。远端工作流、token、environment 审批、上传和两端公开包核验在执行前都属于未验证。CI 中的脚本模型适配器不是实际模型评测,包入口夹具不是桌面安装验收。发布不自动安装或重启 DSH。
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
# 宿主调查与兼容性
|
|
2
|
+
|
|
3
|
+
## 结论:只做插件
|
|
4
|
+
|
|
5
|
+
调查日期:2026-10-15。当前桌面安装的 runtime 和各 DSH 组件均为 **0.2.0-rc.2**,Cordis 4.0.4。源码来自安装 ASAR,只读提取到项目开发证据目录;未改安装文件。插件针对这个精确版本编译。不能据此推断旧版、新版均兼容。
|
|
6
|
+
|
|
7
|
+
**A. 纯插件可实现**:注册工具、同进程异步等待原调用、可信 Agent 归属、排队及幂等请求、调用现有 read 工具全管线、Job 进度与取消。无需模型参与等待。
|
|
8
|
+
|
|
9
|
+
**B. 当前缺少的能力(仅记录,不修改宿主)**:跨进程重新认领原工具调用的公开契约,以及工具结果写入会话日志的持久化确认。若将来宿主提供,至少需要以 session + call-event-seq + callId 进行 CAS 认领、单次提交和可查询提交凭证,且必须在中断恢复合成结果前参与恢复。现在不实现、不伪造这些 API。
|
|
10
|
+
|
|
11
|
+
**C. 本版降级**:受理、队列、结果与交付标记均为内存态。停止/崩溃后不可续接;不会自动重跑。新调用查询只能在当前插件实例的保存期内重取,不能当作恢复原调用。整体交付属于第一阶段、部分完成,不是完整跨重启总线。
|
|
12
|
+
|
|
13
|
+
## 实际调用链和源码位置
|
|
14
|
+
|
|
15
|
+
以下均为本机只读提取的**实际发布构建源码**,不是臆测的源码仓库路径。安装归档无法直接被普通文件工具当目录访问,提取脚本只向本项目写入。
|
|
16
|
+
|
|
17
|
+
1. [Agent 调度器:506–532](../dev/host-snapshot/node_modules/@deepseek-ai/dsh-agent-loop/lib/index.js#L506-L532):requireInitiator 得到 Agent,产生 callId、arguments、agent、signal;runGroup 调度且等待结果。不在等待期间请求模型。
|
|
18
|
+
2. [工具注册:2878–2886](../dev/host-snapshot/node_modules/@deepseek-ai/dsh-tools/lib/index.js#L2878-L2886):ctx.tools.register 是作用域 effect,卸载自动移除。工具 execute 可以返回 Promise。
|
|
19
|
+
3. [可信执行身份:3131–3175](../dev/host-snapshot/node_modules/@deepseek-ai/dsh-tools/lib/index.js#L3131-L3175):注册表生成 token,保留 callId、rootCallId、agent、signal,快照参数。
|
|
20
|
+
4. [前置策略:3225](../dev/host-snapshot/node_modules/@deepseek-ai/dsh-tools/lib/index.js#L3225) → [执行 waterfall 与工具体:3294–3331](../dev/host-snapshot/node_modules/@deepseek-ai/dsh-tools/lib/index.js#L3294-L3331) → [后置策略:3504](../dev/host-snapshot/node_modules/@deepseek-ai/dsh-tools/lib/index.js#L3504) → [最终 tools/result:3417](../dev/host-snapshot/node_modules/@deepseek-ai/dsh-tools/lib/index.js#L3417)。插件只调用公开 execute,不直接调用 read 的定义体,不跳过这些策略。
|
|
21
|
+
5. [可选 PTC:1294–1307](../dev/host-snapshot/node_modules/@deepseek-ai/dsh-tools/lib/index.js#L1294-L1307):子调用继承 agent、rootCallId,带 parent token;在原 run_code 时间预算内等待。[超时策略:4–6](../dev/host-snapshot/node_modules/@deepseek-ai/dsh-tool-call-timeout-policy/lib/index.js#L4-L6) 是协作式取消并 drain,不代表外部副作用失败。插件不改全局工具呈现、不拦截其他工具。
|
|
22
|
+
6. [宿主 read:174](../dev/host-snapshot/node_modules/@deepseek-ai/dsh-tool-fs/lib/index.js#L174):cwd 来自 exec.agent.session.header;[实际读取和输出限制:348](../dev/host-snapshot/node_modules/@deepseek-ai/dsh-tool-fs/lib/index.js#L348)。嵌套 read 在出队时重新进工具管线,沿用当时宿主权限和 fs 实现;插件没有独立 shell、spawn 或文件读取能力。read 的只读访问边界由宿主实际组合的 fs/权限实现决定,不能把 cwd 当作沙箱。隔离测试的 LocalFS 不等于桌面沙箱验证。
|
|
23
|
+
7. [Job start:412–491](../dev/host-snapshot/node_modules/@deepseek-ai/dsh-jobs-local/lib/index.js#L412-L491):已有 owner 校验、controller admission、输出环、进度句柄、首终态约束;[等待:510](../dev/host-snapshot/node_modules/@deepseek-ai/dsh-jobs-local/lib/index.js#L510)、[进度:717–729](../dev/host-snapshot/node_modules/@deepseek-ai/dsh-jobs-local/lib/index.js#L717-L729)。插件只补排队、请求冲突和结果复用,不再造通用 Job 注册表。
|
|
24
|
+
8. [完成通知抑制:268–270](../dev/host-snapshot/node_modules/@deepseek-ai/dsh-tool-jobs/lib/index.js#L268-L270):awaited 为 true 时不追加完成通知;插件先注册 wait 再等待终态,并移除已收集 Job。每 60 秒等待窗口续等由程序执行,不调用模型,不把超时改为失败。
|
|
25
|
+
9. [原调用结果提交:680–706](../dev/host-snapshot/node_modules/@deepseek-ai/dsh-agent-loop/lib/index.js#L680-L706):宿主将结果关联原 callId 和 call-event-seq;插件只 return,不追加 tool/result 或聊天消息。
|
|
26
|
+
10. [中断恢复:717–730](../dev/host-snapshot/node_modules/@deepseek-ai/dsh-session/lib/index.js#L717-L730) 和 [interruptedTurnClosers:904](../dev/host-snapshot/node_modules/@deepseek-ai/dsh-session/lib/index.js#L904):为未保存结果生成 outcome unknown,不会复活内存中的 Promise。[Agent 恢复:1950](../dev/host-snapshot/node_modules/@deepseek-ai/dsh-agent-loop/lib/index.js#L1950) 使用这个保守闭合机制。
|
|
27
|
+
|
|
28
|
+
## 插件扩展方式
|
|
29
|
+
|
|
30
|
+
- 工具:ctx.tools.register;服务:Cordis Service 的标准构造注册(例如 [JobRegistry:106–110](../dev/host-snapshot/node_modules/@deepseek-ai/dsh-jobs/lib/index.js#L106-L110))。本插件没有替换宿主服务。
|
|
31
|
+
- 事件:ctx.on / ctx.waterfall / ctx.emit;副作用生命周期 ctx.effect,异步 disposer 用于取消并等待处理中的任务。本插件未注册全局工具拦截器。
|
|
32
|
+
- 界面:本版不注册自定义前端,复用现有 Jobs 进度事件和默认工具卡片。宿主原有客户端插件体系不是必需依赖;尚未完成该体系全部入口调查,不能提供未核实的 UI 注册示例。
|
|
33
|
+
- 持久化:宿主会话日志有持久化;本地 Job 注册表注释明确是 process-local。二者不能混同为插件可恢复执行权。
|
|
34
|
+
|
|
35
|
+
## 名称核对
|
|
36
|
+
|
|
37
|
+
- [npm 官方接口](https://registry.npmjs.org/@feng0531%2Fdsh-execution-bus) 在调查时返回 404:未发现可见同名包,不代表预留成功或拥有发布权限。
|
|
38
|
+
- 现有本地 origin 是 https://github.com/f-e-n-g-0531/dsh-execution-bus.git ,git ls-remote 读取 HEAD 为 06299e9425d6ba197c0d2e04bf8818ddfe471edf。仓库名称已经存在,与当前工作区既有 remote 一致,未创建、推送或修改远程;GitHub API 限流未拿到账户所有权证明。
|
|
39
|
+
- 中文“执行总线”和英文“Execution Bus”是描述性显示名称,不是唯一名称或商标授权结论。
|
|
40
|
+
|
|
41
|
+
## 验证分层
|
|
42
|
+
|
|
43
|
+
真实 npm 宿主组件隔离冒烟:已运行(真实 ToolRuntime / LocalJobs / read / LocalFS,测试 Agent 夹具)。
|
|
44
|
+
真实 AgentLoop + AgentRegistry + SessionStore 内存会话:脚本 LlmAdapter 隔离测试已通过,原工具成功结果只提交一次,再进入下一适配器请求。不是实际模型评测。真实桌面 GUI、磁盘会话持久化提交、真实模型推理续接、PTC 进程端到端:**未验证**。没有安装日常环境,也没有重启或使用模型凭据。组件测试不能替代这些结论。
|
package/docs/protocol.md
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
# 协议 v1、状态与投递
|
|
2
|
+
|
|
3
|
+
## 输入
|
|
4
|
+
|
|
5
|
+
严格 JSON,最大 16 KiB,不接受额外顶层字段。messageId / requestId 为 1–128 位字母、数字、下划线、点、冒号或短横线。
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
type 为 request 时 payload 为 {handler,input};type 为 status / result / cancel 时 payload 必须为 {}。不支持其他类型或版本。sessionId、权限、工作目录不能出现在输入中,可信归属只能由宿主工具上下文绑定。
|
|
9
|
+
|
|
10
|
+
- messageId 标识一条消息;同请求内同 messageId 不同内容拒绝。消息记账最多 128 条,超过拒绝新消息,不自动遗忘取消记账。
|
|
11
|
+
- requestId 是一次逻辑操作的稳定幂等键;作用域为当前插件实例 + 宿主 session。相同 requestId 不同 handler/input 拒绝冲突,相同输入使用新 requestId 仍是新操作。
|
|
12
|
+
- 输入规范排序后的摘要仅用于同 requestId 冲突比较,不作永久参数去重。保存期后旧键会失效,调用者不得因此盲目重跑有副作用操作。
|
|
13
|
+
- callId / rootCallId / Agent / parent token 来自宿主;通过工具原本的 Promise 关联结果,不依赖模型提供的身份。
|
|
14
|
+
|
|
15
|
+
## 输出
|
|
16
|
+
|
|
17
|
+
核心 submit 返回 accepted 事件和 done Promise。受理只意味着已进入内存记录和队列,durable:false;不是成功。事件包含 protocolVersion、messageId、requestId、correlationId(首次请求 messageId)、sequence、type、payload。每请求 sequence 单调递增,accepted 是当前序号的快照,不占新增序号。
|
|
18
|
+
|
|
19
|
+
事件类型:accepted、started、progress、cancel_requested、succeeded、failed、cancelled、unknown。进度只接受 Handler 正整数序号递增的百分比,重复、乱序、终态后进度丢弃。进程内投递,无外部消息接收端,也不支持任意远端终态注入;终态只由总线持有的执行 Promise 结算。
|
|
20
|
+
|
|
21
|
+
工具的 request 保持 pending,最终返回 RecordView(requestId,state,sequence,cancelRequested,delivery,result?,error?,progress?);status/result/cancel 返回同一种当前快照。工具不会提前返回 accepted 或 taskId,也不会用聊天进度消息唤醒模型。Job 的 UI progress 只含状态/百分比。
|
|
22
|
+
|
|
23
|
+
## 状态转换
|
|
24
|
+
|
|
25
|
+
queued → running → succeeded / failed / cancelled / unknown。
|
|
26
|
+
queued → cancelled。终态不可变;取消终态任务只返回原状态。
|
|
27
|
+
|
|
28
|
+
运行中取消先置 cancelRequested 并触发 AbortSignal,不立即声称 cancelled。Handler 用 AbortError 确认取消后才 cancelled;收到取消仍成功返回则 success 获胜;取消后其他异常为 unknown。Handler 必须合作取消并等待自身资源停止,不能把部分副作用当作已撤销。初版内置 Handler 仅为受控测试和只读宿主 read,没有非幂等写操作。
|
|
29
|
+
|
|
30
|
+
异常是固定错误码,不将异常堆栈/原文写日志。过大或不可保存的结果进入 unknown / RESULT_NOT_RETAINED,不因结果传输失败自动执行一次。执行结束不等于交付,delivery 单独记录 pending / returned;returned 仅代表插件已走到工具返回路径,**不是会话持久化提交确认**。done 快照保留当时的 pending,后续查询可见 returned。宿主可能因取消/后置策略改变最终工具结果,插件不声称已确认模型收到。
|
|
31
|
+
|
|
32
|
+
## 容量和清理
|
|
33
|
+
|
|
34
|
+
默认并发 2,待分发队列 32,记录 256,单结果 JSON 64 KiB;所有请求先经上限检查,满则 BACKPRESSURE,不自动重试。Job 仍受宿主自己的每 owner 容量限制。队列不提供公平性 SLA,单会话可占满实例。
|
|
35
|
+
|
|
36
|
+
终态自完成后保存一小时;新受理时惰性清理,可通过 core.sweep 主动清理。无定时器常驻、无磁盘结果存储、无日志输出。插件关闭先停止受理,取消队列与运行任务,等待合作 Handler,清理内存;不合作的 Handler 会阻碍关闭,插件无法安全硬杀同进程代码。
|
|
37
|
+
|
|
38
|
+
敏感文件结果仍可能进入宿主原有工具会话日志,本插件不具备任意文本秘密识别能力。只请求确实需要的内容;不将敏感内容放入请求标识。结果长度检查发生在 Handler 返回后,扩展 Handler 自身也必须限制分配,不可把此检查当成防止超大对象构造的内存沙箱。
|
|
39
|
+
|
|
40
|
+
## 投递语义、超时和恢复
|
|
41
|
+
|
|
42
|
+
在同一个存活实例/保存期内,同 session + requestId 多次投递共用一次执行;每个实际工具调用独立取得一次响应。这不是端到端 exactly-once,同一请求重复调用仍会增加宿主工具调用记录。终态状态和结算锁避免一次执行被重复完成;不会额外主动提交会话结果。
|
|
43
|
+
|
|
44
|
+
无网络传输层,因此自动通信重试次数为 **0**,也不自动重试执行。宿主 Job 60 秒窗口超时只是程序续等,不改变执行状态、不触发模型查询。PTC 外层仍有自己的时限和取消语义。
|
|
45
|
+
|
|
46
|
+
本版不做持久化:受理/运行/结果/交付任一阶段进程崩溃都会丢失插件记录。宿主日志可能记录请求和结果,不代表插件恢复了执行。跨重启需人工结合宿主 outcome unknown 和外部实际状态处理;不能自动重跑非幂等操作。本版不实现对账钩子或跨进程重试,扩展有副作用 Handler 前必须另行设计这些能力。
|
|
47
|
+
|
|
48
|
+
## 扩展 Handler
|
|
49
|
+
|
|
50
|
+
core 导出 ExecutionBus、Handler 和 Capability 类型。register(name,handler) 返回注销函数;同名拒绝。Handler 提供 validate 和 async run,执行上下文仅有 requestId、signal、progress 与最小执行 capability,不传聊天历史。注销不影响已受理任务捕获的 Handler;关闭时等待它们。
|
|
51
|
+
|
|
52
|
+
当前插件内置注册 test 与 host.read。尚未公开 Cordis Handler 注册服务,其他插件不能假定存在 ctx.executionBus;跨插件动态注册属于后续事项。
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# 执行总线 0.1.0
|
|
2
|
+
|
|
3
|
+
## 本版变化
|
|
4
|
+
|
|
5
|
+
第一阶段纯插件原型:版本化协议、有界队列、请求复用与冲突检查、原工具异步等待、宿主 Job 进度、受控测试 Handler 和宿主只读 read 接入。测试与发布流程参考 dsh-vcs,标签触发四矩阵 CI,正式发布独立手动门禁。
|
|
6
|
+
|
|
7
|
+
## 安装
|
|
8
|
+
|
|
9
|
+
提供 npm 包 @feng0531/dsh-execution-bus@0.1.0 和同包 GitHub Release 安装制品;发布过程不安装到日常 DSH。兼容目标 DSH 0.2.0-rc.2。请先阅读 README 和宿主兼容说明,选择隔离配置。
|
|
10
|
+
|
|
11
|
+
## 限制
|
|
12
|
+
|
|
13
|
+
非持久化、不支持跨重启原调用恢复,不提供任意命令执行。真实模型、GUI、桌面沙箱、PTC 全链路尚未验证。采用非商业源码公开许可证,非 OSI 开源许可;商业项目内部使用及有偿交付需要另行书面授权。
|
|
14
|
+
|
|
15
|
+
## 验证
|
|
16
|
+
|
|
17
|
+
真实宿主组件与 AgentLoop 内存会话采用脚本模型隔离测试;不能当作真实模型端到端通过。具体本地测试数与检查结果见 docs/validation.md。2026-10-15 正式发布完成:Windows/Ubuntu × Node 22/24 四项标签 CI 全部通过,发布工作流成功。npm latest 为 0.1.0;npm 与 GitHub 实际下载包均为 22,017 字节,与候选逐字节一致,SHA-256 为 9b5f57d829c2238d0bed8548e17633a67b15a5358b792df0d492071a261529ce。
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
# 执行总线 0.1.1
|
|
2
|
+
|
|
3
|
+
## 本版变化
|
|
4
|
+
|
|
5
|
+
本版为 BUG 修复,无新增业务执行能力。
|
|
6
|
+
|
|
7
|
+
- 修复 core 多个结果消费者共享可修改终态快照,可能污染后续重复请求结果的问题;终态快照改为深度冻结。
|
|
8
|
+
- 修复 Handler 已完成但返回值无法 JSON 序列化时被误记执行失败的问题;记录为 unknown / RESULT_NOT_RETAINED,不自动重跑。
|
|
9
|
+
- 修复没有新请求时过期结果仍能查询/重取的问题;控制消息入口补上 TTL 清理。
|
|
10
|
+
- 增加不安装插件的可复用流程测试命令 npm run test:flow,以及三项修复前失败、修复后通过的回归测试。
|
|
11
|
+
|
|
12
|
+
## 安装与许可
|
|
13
|
+
|
|
14
|
+
npm 包 @feng0531/dsh-execution-bus@0.1.1,兼容目标 DSH 0.2.0-rc.2;建议先用隔离配置。非商业源码公开许可,具体以 LICENSE 为准。发布不自动安装、升级或重启日常 DSH。
|
|
15
|
+
|
|
16
|
+
## 验证与限制
|
|
17
|
+
|
|
18
|
+
本地全量 24/24 测试通过;真实宿主组件、AgentLoop 内存会话均为隔离测试,模型侧使用脚本适配器,不等于当前会话直接调用 execution_bus。真实模型、GUI、PTC 和桌面沙箱未验收;不持久化、不支持跨重启恢复。正式发布仍需同标签四矩阵 CI、包白名单、候选一致性、隔离安装与公开 npm 包核验全部通过。
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
# 验证记录与交付状态
|
|
2
|
+
|
|
3
|
+
日期:2026-10-15。Node 24.21.0,Windows,宿主精确兼容目标 0.2.0-rc.2。全部开发改动仅在当前独立仓库,未修改/重启 DSH,未发布 npm、未推送远程、未安装用户日常配置。
|
|
4
|
+
|
|
5
|
+
## 自动化测试
|
|
6
|
+
|
|
7
|
+
npm test:**21/21 通过**。本地 tgz 已在 artifacts/package-check 独立目录完成 npm 安装及入口 import 验证(不是 DSH 插件管理器安装)。测试名称是断言集合,不等于 21 项完整产品验收。
|
|
8
|
+
|
|
9
|
+
- 正常请求、受理不冒充成功、原 Promise 等待、终态只有一次、交付标记分离。
|
|
10
|
+
- 两个 session 同 requestId 结果独立,第三会话查询/取消拒绝。
|
|
11
|
+
- 同请求共用执行,同键不同输入冲突;同参数新键为不同操作。
|
|
12
|
+
- 重复/乱序/迟到进度丢弃,观察者抛错不影响执行。
|
|
13
|
+
- 等待时间短于执行时仍保持运行,不生成失败。
|
|
14
|
+
- 排队取消、运行取消、晚取消、完成竞争;取消未确认为 unknown;unknown 不自动重跑。
|
|
15
|
+
- 队列/记录容量背压、并发为一时第二任务保持 queued、控制消息上限。
|
|
16
|
+
- Handler 异常、未知 Handler、未知消息类型/版本、伪造身份字段、输入越界、结果过大、异常敏感文本不泄漏。
|
|
17
|
+
- 关闭合作任务并拒绝新受理;终态 TTL 清理,不误清理运行任务。
|
|
18
|
+
|
|
19
|
+
真实宿主**组件**隔离测试使用 npm 原版 Cordis + ToolRuntime + LocalJobs + LocalFS + tool-fs。Agent/agents 注册表是夹具,没有运行真实 AgentLoop 或模型。测试通过实际 read 工具读取本仓库包声明,通过真实 pre-execute 拒绝读操作,确认权限管线仍在;测试进度事件、awaited 终态、Job 移除、重复请求并发、调用中断、插件作用域卸载和工具注销。
|
|
20
|
+
|
|
21
|
+
## 真实宿主循环隔离验证
|
|
22
|
+
|
|
23
|
+
另有独立测试运行真实 AgentLoop、AgentRegistry、SessionStore、ToolRuntime 和 Jobs;模型侧是确定性 Scripted LlmAdapter,不访问网络或凭据。适配器先产生一次 execution_bus 请求,宿主等待成功终态并记录一次 tool/result,toolCallId 为 original-call,随后才进行第二次适配器调用并完成回答。无模型轮询、无额外聊天进度消息。这是实际宿主循环和内存会话闭环,不是实际模型评测,也没有验证磁盘持久化。
|
|
24
|
+
|
|
25
|
+
## 未验证(不是通过)
|
|
26
|
+
|
|
27
|
+
| 项目 | 状态与原因 |
|
|
28
|
+
|---|---|
|
|
29
|
+
| 当前桌面真实模型 → 原调用 → 结果 → 模型继续 | 未验证;未安装到日常宿主,也未启用真实模型凭据 |
|
|
30
|
+
| GUI Jobs 实际可视展示 | 未验证;仅证明进度事件进入真实 Job 注册表 |
|
|
31
|
+
| 当前 Windows 授权提示和文件沙箱集成 | 未验证;测试使用 LocalFS 和前置拒绝策略,不代表桌面沙箱 |
|
|
32
|
+
| PTC 完整进程入口和超时 | 源码核查/类型兼容;未执行真实 PTC 冒烟 |
|
|
33
|
+
| 真实模型面对重复终态的继续行为 | 真实循环的单次结果提交已验证;真实模型及外部终态重投链路未验证 |
|
|
34
|
+
| 跨重启恢复原调用 | 不支持,不能用新消息冒充 |
|
|
35
|
+
| 受理/运行/结果/交付四阶段持久化崩溃注入 | 未实现持久化,不声称通过;当前实例丢失的边界测试通过 |
|
|
36
|
+
| 模型轮次、主动查询、重复执行、总耗时对照 | 全部未验证;不捏造提升比例 |
|
|
37
|
+
| 插件管理器完整安装、卸载与回滚 | 未验证;仅包构建/内容检查及 Cordis effect 注销 |
|
|
38
|
+
|
|
39
|
+
## CI 与发布流程检查
|
|
40
|
+
|
|
41
|
+
参考 dsh-vcs 配置标签/手动 CI:Windows/Ubuntu × Node 22/24;普通分支和 PR 不触发。新增版本/锁文件/标签一致性、包严格白名单、全新临时目录生产依赖安装、工作流 YAML 契约和许可证缺失拒绝测试。本地 Windows/Node 24 的 check、verify:package、pack:candidate、test:install 通过,npm audit 为 0 漏洞。
|
|
42
|
+
|
|
43
|
+
发布仅手动确认后运行,需精确标签 CI 成功且四份候选与重建包逐字节一致;先 npm 上传并验证公开实际字节,再创建 GitHub Release。用户已确认非商业许可证并授权正式发布;预检仍验证明确确认词及完整 LICENSE。2026-10-15:v0.1.0 标签 CI 四矩阵成功(运行 38705851984),联合发布成功(38706021548)。GitHub Release 非草稿、非预发布;npm latest 为 0.1.0。公开 npm/GitHub 包均为 22,017 字节,与候选逐字节一致,SHA-256:9b5f57d829c2238d0bed8548e17633a67b15a5358b792df0d492071a261529ce。npm-release 环境此前不存在,未配置维护者保护审批,发布使用用户授权及工作流明确确认门禁。
|
|
44
|
+
|
|
45
|
+
## 已完成 / 部分完成 / 后续
|
|
46
|
+
|
|
47
|
+
已完成:可编译独立插件、协议/状态核心、测试 Handler、真实只读执行管线适配、宿主 Job 进度事件、自动化测试和中文文档。
|
|
48
|
+
|
|
49
|
+
部分完成:会话闭环已验证到真实 AgentLoop/内存 Session,使用脚本适配器;真实模型与磁盘会话仍未验证。显示名称体现在工具说明和 Job 标签,未提供插件市场 UI 元数据。Handler core 可扩展,尚无跨插件服务。
|
|
50
|
+
|
|
51
|
+
未实现:持久化、跨重启重关联、非幂等 Handler 对账、远端传输与通信重试(当前零重试)、自定义 UI。只做插件的约束保持不变。
|
|
52
|
+
|
|
53
|
+
下一步应先在用户明确授权的隔离 DSH 配置进行真实模型/AgentLoop/PTC/GUI 冒烟,再决定持久化范围。不应先上线任意命令或有副作用执行 Handler。
|
|
54
|
+
|
|
55
|
+
## 已知限制
|
|
56
|
+
|
|
57
|
+
- 并发重复附着现在共享一个宿主 Job 和程序等待者;回归测试断言只新增一个 Job。Job admission 首次失败会取消尚未执行的受理请求。保存期内已完成请求的再次重取可能建立短暂 Job,但不会重新执行 Handler。
|
|
58
|
+
- 同一逻辑请求的任一等待工具被取消,会请求取消共享执行;这是本版明确的请求级取消规则,不是独立等待者取消。
|
|
59
|
+
- returned 不证明宿主已经持久化或模型已经消费,取消/后置策略仍可能改变最终宿主结果。
|
|
60
|
+
- 核心不是独立安全边界;可信插件可以注册 Handler,恶意同进程插件不在威胁模型内。
|
|
61
|
+
- 本地源码证据快照不包含在 npm 包中;文档相对证据链接适用于开发仓库提取后阅读,安装包读者应使用所列组件名及构建行号查对应版本。
|
package/package.json
CHANGED
|
@@ -1,6 +1,54 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@feng0531/dsh-execution-bus",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"version": "0.1.1",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"private": false,
|
|
6
|
+
"license": "SEE LICENSE IN LICENSE",
|
|
7
|
+
"publishConfig": { "access": "public", "registry": "https://registry.npmjs.org/" },
|
|
8
|
+
"repository": { "type": "git", "url": "git+https://github.com/f-e-n-g-0531/dsh-execution-bus.git" },
|
|
9
|
+
"description": "执行总线 / Execution Bus: bounded DSH handler dispatch with original-call waiting",
|
|
10
|
+
"main": "dist/plugin.js",
|
|
11
|
+
"exports": {
|
|
12
|
+
".": "./dist/plugin.js",
|
|
13
|
+
"./core": "./dist/bus.js"
|
|
14
|
+
},
|
|
15
|
+
"files": [
|
|
16
|
+
"dist",
|
|
17
|
+
"LICENSE",
|
|
18
|
+
"THIRD_PARTY_NOTICES.md",
|
|
19
|
+
"README.md",
|
|
20
|
+
"docs"
|
|
21
|
+
],
|
|
22
|
+
"engines": {
|
|
23
|
+
"node": ">=22"
|
|
24
|
+
},
|
|
25
|
+
"scripts": {
|
|
26
|
+
"build": "tsc",
|
|
27
|
+
"test": "npm run build && node --test test/*.test.mjs",
|
|
28
|
+
"pack": "npm pack",
|
|
29
|
+
"check": "npm test",
|
|
30
|
+
"test:flow": "npm run build && node --test test/flow-regression.test.mjs test/host.test.mjs test/agent-loop.test.mjs",
|
|
31
|
+
"prepack": "npm run check && npm run verify:package",
|
|
32
|
+
"verify:package": "node scripts/verify-package.mjs",
|
|
33
|
+
"test:install": "node scripts/test-install.mjs",
|
|
34
|
+
"pack:candidate": "node scripts/pack.mjs",
|
|
35
|
+
"verify:release": "node scripts/release-preflight.mjs"
|
|
36
|
+
},
|
|
37
|
+
"peerDependencies": {
|
|
38
|
+
"@deepseek-ai/cordis": "~4.0.4",
|
|
39
|
+
"@deepseek-ai/dsh-jobs": "0.2.0-rc.2",
|
|
40
|
+
"@deepseek-ai/dsh-tools": "0.2.0-rc.2"
|
|
41
|
+
},
|
|
42
|
+
"devDependencies": {
|
|
43
|
+
"@deepseek-ai/dsh-agent-loop": "0.2.0-rc.2",
|
|
44
|
+
"@deepseek-ai/dsh-fs-local": "0.2.0-rc.2",
|
|
45
|
+
"@deepseek-ai/dsh-jobs": "0.2.0-rc.2",
|
|
46
|
+
"@deepseek-ai/dsh-jobs-local": "0.2.0-rc.2",
|
|
47
|
+
"@deepseek-ai/dsh-tool-fs": "0.2.0-rc.2",
|
|
48
|
+
"@deepseek-ai/dsh-tool-jobs": "0.2.0-rc.2",
|
|
49
|
+
"@types/node": "^22",
|
|
50
|
+
"prettier": "3.6.2",
|
|
51
|
+
"typescript": "~5.9.3",
|
|
52
|
+
"yaml": "2.9.1"
|
|
53
|
+
}
|
|
54
|
+
}
|