@meiyukichan/dsh-sandore 0.1.2-rc.2 → 0.1.2-rc.3
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/lib/client/better-sidebar-open.d.ts +7 -1
- package/lib/client/better-sidebar-open.js +4 -3
- package/lib/client/index.js +3 -3
- package/lib/index.js +159 -74
- package/lib/workflow/capability-registry.d.ts +5 -5
- package/lib/workflow/domain.d.ts +7 -7
- package/lib/workflow/instance-output.d.ts +98 -49
- package/lib/workflow/instance-output.js +136 -50
- package/lib/workflow/list-instances-tool.d.ts +3 -3
- package/lib/workflow/remote.d.ts +15 -13
- package/lib/workflow/remote.js +27 -19
- package/lib/workflow/status-tool.d.ts +1 -1
- package/lib/workflow/wire.d.ts +5 -4
- package/lib/workflow/wire.js +5 -4
- package/package.json +1 -1
- package/packages/client/better-sidebar-open.spec.ts +26 -17
- package/packages/client/better-sidebar-open.ts +11 -4
- package/packages/client/index.ts +3 -3
- package/packages/client/orchestration-tool-card.spec.ts +2 -2
- package/packages/client/run-view-host-filter.spec.ts +2 -2
- package/packages/resources/skills/sandore-workflow-authoring/SKILL.md +46 -7
- package/packages/resources/skills/sandore-workflow-authoring/reference/authoring-workflow.md +24 -0
- package/packages/resources/skills/sandore-workflow-authoring/reference/index.md +1 -0
- package/packages/workflow/instance-output.spec.ts +113 -26
- package/packages/workflow/instance-output.ts +159 -51
- package/packages/workflow/remote.spec.ts +58 -20
- package/packages/workflow/remote.ts +27 -19
- package/packages/workflow/wire.ts +5 -4
|
@@ -236,7 +236,13 @@ export interface SandoreFileOpenDeps {
|
|
|
236
236
|
* (`openpath-intercept.ts:23-27` 明说),缓存旧实例会让 shadow 后的新实例被绕过。
|
|
237
237
|
*/
|
|
238
238
|
readonly sessionRemote?: (() => SessionRemoteLike | undefined) | undefined;
|
|
239
|
-
/**
|
|
239
|
+
/**
|
|
240
|
+
* host 侧落盘 RPC(写 `<DSH_HOME>/storages/sandore/output/<instanceId>/…`,返回**进程绝对路径**)。
|
|
241
|
+
*
|
|
242
|
+
* 该落点**在会话 workspace 之外**(本批从 `<cwd>/.sandore/artifacts/` 搬出来)⇒ L3 的越界判据
|
|
243
|
+
* 对它恒成立,所以 better-sidebar 在场时 Agent 输出一律走自注册 tab(`sandore-tab`),
|
|
244
|
+
* 不再落进用户的仓库。
|
|
245
|
+
*/
|
|
240
246
|
readonly writeOutput?: ((request: WriteOutputRequest) => Promise<string>) | undefined;
|
|
241
247
|
/** host 侧读文件 RPC(`readInstanceFile`;应用内兜底与 C6 tab 体共用的取数面)。 */
|
|
242
248
|
readonly readFile?: ((request: ReadFileRequest) => Promise<ReadFileResultLike>) | undefined;
|
|
@@ -683,9 +683,10 @@ export function installSandoreFileOpen(deps) {
|
|
|
683
683
|
* 内存里,不落盘就没有 path ⇒ 只能自家渲染 ⇒ 正是用户不接受的那件事。
|
|
684
684
|
* 触发条件:**确认至少有一条落点通道**(`canHandOff`)。它不成立(服务缺席 / 无活跃会话 /
|
|
685
685
|
* 无 openTab / 无漏斗)⇒ **零写盘**,直接用内存文本走 L4。
|
|
686
|
-
* 为什么这不算"偷偷写文件":①
|
|
687
|
-
*
|
|
688
|
-
*
|
|
686
|
+
* 为什么这不算"偷偷写文件":① 落点固定且**在我们自己的数据目录**里
|
|
687
|
+
* (`<DSH_HOME>/storages/sandore/output/<instanceId>/`,与 `sandore.sqlite` / 只读锁同一个家
|
|
688
|
+
* —— 不往用户的项目工作区写);② 文件名由 step/attempt 派生 ⇒ **重复点击是覆盖,不是堆积**;
|
|
689
|
+
* ③ 写的是用户**刚刚点击**要看的那一段输出(不是后台产物)。
|
|
689
690
|
*/
|
|
690
691
|
if (!canHandOff()) {
|
|
691
692
|
const failure = landingFailureOf();
|
package/lib/client/index.js
CHANGED
|
@@ -122,9 +122,9 @@ export async function apply(ctx) {
|
|
|
122
122
|
sessions: sessionsCwdSource,
|
|
123
123
|
// L1 的现取(`remote.session` 的漏斗 + 它的能力探针 `canOpenWorkspacePath`)。
|
|
124
124
|
sessionRemote: () => sessionRemoteCtx?.get('remote.session'),
|
|
125
|
-
// Agent 输出落盘:host
|
|
126
|
-
//
|
|
127
|
-
//
|
|
125
|
+
// Agent 输出落盘:host 唯一构造落点(`<DSH_HOME>/storages/sandore/output/<instanceId>/…`,
|
|
126
|
+
// **在会话 workspace 之外**),client 只拿回真实绝对路径再交给 `openTab`。**它只在确认要交给
|
|
127
|
+
// better-sidebar 时才写**(没 path 就没有 tab 的内容种子,见 `openOutput` 的结论注释)。
|
|
128
128
|
// 显式给 `unwrap` 类型实参:`remoteCtx.remote.sandore` 的成员在客户端图里是 `any`
|
|
129
129
|
// (cordis 注入 ctx 的 `remote` 面不参与类型收窄),而 `unwrap(any)` 会退化成
|
|
130
130
|
// `unknown`(无法从 `any` 反推 T)⇒ 不开显式实参就取不到 `.path`。
|
package/lib/index.js
CHANGED
|
@@ -6,11 +6,11 @@ import { Service } from "@deepseek-ai/cordis";
|
|
|
6
6
|
import z$1 from "@deepseek-ai/schemastery";
|
|
7
7
|
import { SessionId, SessionSeq } from "@deepseek-ai/dsh-session";
|
|
8
8
|
import { Remote, TypertRemoteService } from "@deepseek-ai/dsh-typert-protocol";
|
|
9
|
+
import { homedir } from "node:os";
|
|
10
|
+
import { basename, dirname, join, resolve } from "node:path";
|
|
9
11
|
import { installModelSelection } from "@deepseek-ai/dsh-agent";
|
|
10
12
|
import { randomUUID } from "node:crypto";
|
|
11
13
|
import { mkdir, readFile, rename, rm, writeFile } from "node:fs/promises";
|
|
12
|
-
import { homedir } from "node:os";
|
|
13
|
-
import { basename, dirname, join, resolve } from "node:path";
|
|
14
14
|
import { setTimeout as setTimeout$1 } from "node:timers/promises";
|
|
15
15
|
import { existsSync, readFileSync, readdirSync } from "node:fs";
|
|
16
16
|
import { fileURLToPath } from "node:url";
|
|
@@ -5406,73 +5406,126 @@ function withMicroflow(definition, microflow, entryId, base) {
|
|
|
5406
5406
|
//#endregion
|
|
5407
5407
|
//#region packages/workflow/instance-output.ts
|
|
5408
5408
|
/**
|
|
5409
|
-
* **Agent 输出落盘**(result 叶子 →
|
|
5409
|
+
* **Agent 输出落盘**(result 叶子 → 磁盘上的真实文件)。
|
|
5410
5410
|
*
|
|
5411
5411
|
* ## 为什么需要落盘
|
|
5412
5412
|
* 工作流导航树里 **result 叶子("Agent 输出")只有文本、没有文件**(`run-view-model.ts`
|
|
5413
|
-
* 的 `buildRunTree` 从 `executedSteps[].output` 现算),而"打开文件"
|
|
5414
|
-
* 直接决定用户体验:
|
|
5415
|
-
*
|
|
5416
|
-
* - **落 workspace 内**(本模块)= 零配置:better-sidebar 的 `workspaceFence`(0.18.0 默认
|
|
5417
|
-
* 开启)只放行会话 cwd 之内的路径 ⇒ 打开即读得到内容;
|
|
5418
|
-
* - 落系统临时目录 = 用户每次都要去设置页手动关掉**全局** fence(并承担"任何同源脚本可越界
|
|
5419
|
-
* 读写"的代价),且**部署侧没有任何开关能预先关掉它**(`fenceEnabledOf` 只读 settings 服务,
|
|
5420
|
-
* 不吃插件 config)—— 故不采用。
|
|
5413
|
+
* 的 `buildRunTree` 从 `executedSteps[].output` 现算),而"打开文件"必须有文件。
|
|
5421
5414
|
*
|
|
5422
|
-
* ##
|
|
5415
|
+
* ## 落点:**DSH home 下 sandore 自己的存储目录**(定稿,别再放回 workspace)
|
|
5423
5416
|
* ```
|
|
5424
|
-
* <
|
|
5417
|
+
* <DSH_HOME>/storages/sandore/output/<instanceId>/<stepId>-<attemptIndex>-output.md
|
|
5425
5418
|
* ```
|
|
5426
5419
|
* | 路径段 | 为什么必需 |
|
|
5427
5420
|
* |---|---|
|
|
5428
|
-
* |
|
|
5421
|
+
* | `<DSH_HOME>/storages/sandore/` | 与 `storages/sandore.sqlite` / `storages/sandore-lock.json` **同一个家**:都是「sandore 自己的数据」,**不再往用户的项目工作区里写任何东西**(旧落点 `<实例 cwd>/.sandore/artifacts/` 会污染每个跑过工作流的仓库,还要用户自己加 `.gitignore`) |
|
|
5422
|
+
* | `output/` | 与其它 storage 子目录分开,便于整体清理("Agent 输出"这一类可以一次性删空而不碰定义/锁) |
|
|
5429
5423
|
* | **`<instanceId>/`** | `stepId` 是**定义内 id**:同一工作流跑多个实例时必然重名,没有实例维度会让多实例并发的同一 `stepId` 输出**互相覆盖**(`instanceId ≡ 宿主会话 id`,见 AGENTS.md 的实例身份恒等式) |
|
|
5430
5424
|
* | `<stepId>-<attemptIndex>-output.md` | 同一实例内同一 step 的**多次执行段/重试**靠 `attemptIndex` 区分(与 `run-view-model.ts` 的段快照语义一致) |
|
|
5431
5425
|
*
|
|
5432
|
-
*
|
|
5433
|
-
*
|
|
5426
|
+
* **不主动清理**:产出是用户的证据,删掉比留下更危险;清理权交给用户(清理面 =
|
|
5427
|
+
* `<DSH_HOME>/storages/sandore/output/` 整个目录)。
|
|
5434
5428
|
*
|
|
5435
|
-
*
|
|
5436
|
-
* `
|
|
5429
|
+
* ## 路径解析(绝不硬编码绝对路径)
|
|
5430
|
+
* 三级,与 `definition-lock.ts` 的 `resolveDefinitionLockPath` **同口径**:
|
|
5431
|
+
* ① 显式覆盖(测试 / 非常规部署)→ ② DSH 在 root ctx 上 `provide` 的
|
|
5432
|
+
* `dshHomePath('storages','sandore','output')` → ③ 本地回退(`$DSH_HOME`(trim 后非空)> `~/.dsh`)。
|
|
5433
|
+
* 第三级永不失败(总有一个字符串结果),**绝不返回 undefined/空串**(那会让落点退化成"写当前目录"
|
|
5434
|
+
* 这类意外位置)。
|
|
5437
5435
|
*
|
|
5438
|
-
*
|
|
5436
|
+
* > ⚠️ 该回退与 `definition-lock.ts` 的 `fallbackDshHome` 是**同一套规则的两处实现**
|
|
5437
|
+
* > (两个模块都不依赖 `@deepseek-ai/dsh-home-paths`——本包 `node_modules` 里没有它)。
|
|
5438
|
+
* > 两边都有锁(`instance-output.spec.ts` / `definition-lock.spec.ts`),改规则要同时改。
|
|
5439
5439
|
*
|
|
5440
|
-
* `ctx.fs
|
|
5441
|
-
*
|
|
5440
|
+
* ## 写盘走 `ctx.fs`,但**必须显式给 per-call 沙箱策略**(P14 真机报障的根因)
|
|
5441
|
+
* `ctx.fs` 若是沙箱后端(`@deepseek-ai/dsh-fs-sandbox` 的 `SandboxedFileSystem`),它的写盘判据是
|
|
5442
|
+
* (`packages/fs/fs-sandbox/src/index.ts:122-142`):
|
|
5442
5443
|
* ```
|
|
5443
|
-
* const policy = sandboxPolicy ?? this.ctx.sandboxPolicy.resolve() // ←
|
|
5444
|
+
* const policy = sandboxPolicy ?? this.ctx.sandboxPolicy.resolve() // ← 不传就用**无会话**的兜底
|
|
5444
5445
|
* ```
|
|
5445
|
-
* 而无会话的 `resolve()`(`packages/sandbox/sandbox-policy/src/index.ts:163-170`)把
|
|
5446
|
-
*
|
|
5447
|
-
*
|
|
5448
|
-
*
|
|
5449
|
-
*
|
|
5450
|
-
* `
|
|
5451
|
-
*
|
|
5452
|
-
*
|
|
5453
|
-
*
|
|
5454
|
-
*
|
|
5455
|
-
*
|
|
5446
|
+
* 而无会话的 `resolve()`(`packages/sandbox/sandbox-policy/src/index.ts:163-170`)把 `workspaceRoot`
|
|
5447
|
+
* 取成**部署兜底根**(`config.workspaceRoot ?? process.cwd()`);`workspace-write` 的可写根集合 =
|
|
5448
|
+
* `[policy.workspaceRoot, '/tmp', tmpdir()]`(`packages/sandbox/sandbox/src/roots.ts:52-54`)。
|
|
5449
|
+
*
|
|
5450
|
+
* ⇒ 本批落点在 workspace **之外**,所以"解析出会话策略"还不够:还必须把**落点根**作为
|
|
5451
|
+
* `workspaceRoot` 交下去,否则 `workspace-write` 一定拒(真机形态正是
|
|
5452
|
+
* `cannot write "…": file access denied under workspace-write mode`)。见 {@link outputWritePolicy}。
|
|
5453
|
+
*
|
|
5454
|
+
* 三条纪律:
|
|
5455
|
+
* 1. **本模块自己拥有"落点根"**({@link resolveOutputRoot}),**不向调用方要 cwd** —— 落点与
|
|
5456
|
+
* 实例 cwd 无关,要 cwd 只会让将来又漂回 workspace;
|
|
5457
|
+
* 2. 策略取不到(无沙箱后端的裸 `LocalFileSystem`)⇒ 第 5 参不传(后端本来就不判);
|
|
5458
|
+
* 3. 被拒时由调用方折成 `INVALID_OPERATION` + 带**落点路径**的可见原因,
|
|
5459
|
+
* **绝不把内部错原文抛给 UI**。
|
|
5456
5460
|
* @module @meiyukichan/dsh-sandore/workflow/src/instance-output
|
|
5457
5461
|
*/
|
|
5458
|
-
/**
|
|
5459
|
-
const
|
|
5462
|
+
/** DSH home 目录名(`$DSH_HOME` 缺省时用它;与 DSH `home-paths` 的 `DSH_HOME_DIR_NAME` 同值)。 */
|
|
5463
|
+
const DSH_HOME_DIR_NAME$1 = ".dsh";
|
|
5464
|
+
/** 覆盖 DSH home 的环境变量名(与 DSH `home-paths` 的 `DSH_HOME_ENV` 同值)。 */
|
|
5465
|
+
const DSH_HOME_ENV$1 = "DSH_HOME";
|
|
5466
|
+
/** 落点在 DSH home 下的目录段(`<DSH_HOME>/storages/sandore/output`)。 */
|
|
5467
|
+
const OUTPUT_ROOT_SEGMENTS = [
|
|
5468
|
+
"storages",
|
|
5469
|
+
"sandore",
|
|
5470
|
+
"output"
|
|
5471
|
+
];
|
|
5472
|
+
/** 非空(trim 后)才算数;空白串视为"没给"。 */
|
|
5473
|
+
function nonBlank$1(value) {
|
|
5474
|
+
if (value === void 0) return void 0;
|
|
5475
|
+
return value.trim().length === 0 ? void 0 : value;
|
|
5476
|
+
}
|
|
5477
|
+
/** `~` / `~/x` 展开成 home 下的路径。 */
|
|
5478
|
+
function expandHomePath$1(path) {
|
|
5479
|
+
if (path === "~") return homedir();
|
|
5480
|
+
if (path.startsWith("~/") || path.startsWith("~\\")) return join(homedir(), path.slice(2));
|
|
5481
|
+
return path;
|
|
5482
|
+
}
|
|
5460
5483
|
/**
|
|
5461
|
-
*
|
|
5484
|
+
* 本地回退解析 DSH home(`$DSH_HOME`(trim 后非空)> `<homeDir|os.homedir()>/.dsh`)。
|
|
5485
|
+
*
|
|
5486
|
+
* 与 `definition-lock.ts` 的 `fallbackDshHome` 同规则(那边有逐条出处注释);生产路径上优先走
|
|
5487
|
+
* {@link OutputRootOptions.dshHomePath} 注入函数,那时**这套回退根本不参与**。
|
|
5488
|
+
* @param options - 解析入参。
|
|
5489
|
+
* @returns 归一化后的绝对 home 路径。
|
|
5490
|
+
*/
|
|
5491
|
+
function fallbackDshHome$1(options) {
|
|
5492
|
+
const selected = nonBlank$1((options.env ?? process.env)[DSH_HOME_ENV$1]) ?? join(options.homeDir ?? homedir(), DSH_HOME_DIR_NAME$1);
|
|
5493
|
+
return resolve(expandHomePath$1(selected));
|
|
5494
|
+
}
|
|
5495
|
+
/** 统一成 `/` 分隔(落点是**契约值**:调用方拿它拼 URL/展示,不随平台变)。 */
|
|
5496
|
+
function toSlashes(path) {
|
|
5497
|
+
return path.replace(/\\/g, "/");
|
|
5498
|
+
}
|
|
5499
|
+
/**
|
|
5500
|
+
* **纯函数**:解析 Agent 输出的落点根(`<DSH_HOME>/storages/sandore/output`)。
|
|
5501
|
+
*
|
|
5502
|
+
* 三级优先级与 `resolveDefinitionLockPath` 同口径(见模块头)。**永不失败**:拿不到注入函数只是
|
|
5503
|
+
* 回退到本地规则,**绝不**返回 `undefined`/空串。
|
|
5504
|
+
* @param options - 解析入参;缺省 = 只剩本地回退。
|
|
5505
|
+
* @returns 落点根的绝对路径(统一 `/` 分隔;真正的绝对化由 `fs.resolve` 完成)。
|
|
5506
|
+
*/
|
|
5507
|
+
function resolveOutputRoot(options = {}) {
|
|
5508
|
+
const explicit = nonBlank$1(options.explicitRoot);
|
|
5509
|
+
if (explicit !== void 0) return toSlashes(explicit);
|
|
5510
|
+
if (typeof options.dshHomePath === "function") return toSlashes(options.dshHomePath(...OUTPUT_ROOT_SEGMENTS));
|
|
5511
|
+
return toSlashes(join(fallbackDshHome$1(options), ...OUTPUT_ROOT_SEGMENTS));
|
|
5512
|
+
}
|
|
5513
|
+
/**
|
|
5514
|
+
* **纯函数**:解析一次写盘"应当使用的会话策略"。
|
|
5462
5515
|
*
|
|
5463
5516
|
* 判据与 DSH 工具层逐字同源:`ctx.sandboxPolicy.resolve({ session })`,而 `session` 取
|
|
5464
|
-
* **该实例的宿主会话**(恒等式 `instanceId ≡ 宿主会话 id
|
|
5465
|
-
*
|
|
5517
|
+
* **该实例的宿主会话**(恒等式 `instanceId ≡ 宿主会话 id`)——`mode` 由那个会话决定,
|
|
5518
|
+
* 它是**用户的意图**(read-only / workspace-write / danger-full-access),必须尊重。
|
|
5466
5519
|
*
|
|
5467
5520
|
* 三条"取不到就不假装知道"的纪律:
|
|
5468
5521
|
* ① 部署没有沙箱后端(`sandboxPolicy` 缺席 = 裸 `LocalFileSystem`)⇒ `undefined`
|
|
5469
5522
|
* (第 5 参不传,后端本来就不判);
|
|
5470
5523
|
* ② 服务在但**会话取不到** ⇒ 也 `undefined`(不自己编一个 `workspaceRoot`:编错了就是
|
|
5471
|
-
* "绕开用户的文件策略"
|
|
5524
|
+
* "绕开用户的文件策略",编窄了就是 P14 报障);
|
|
5472
5525
|
* ③ `resolve()` 抛错(服务面畸形)⇒ `undefined`(由 `ctx.fs` 自己的兜底判据决定,绝不抛穿)。
|
|
5473
5526
|
* @param deps - 两个服务(都可缺席)。
|
|
5474
5527
|
* @param instanceId - 实例 id(≡ 宿主会话 id)。
|
|
5475
|
-
* @returns
|
|
5528
|
+
* @returns 该会话的策略;无法确定时 `undefined`。
|
|
5476
5529
|
*/
|
|
5477
5530
|
function resolveWritePolicy(deps, instanceId) {
|
|
5478
5531
|
const service = deps.sandboxPolicy;
|
|
@@ -5486,6 +5539,32 @@ function resolveWritePolicy(deps, instanceId) {
|
|
|
5486
5539
|
}
|
|
5487
5540
|
}
|
|
5488
5541
|
/**
|
|
5542
|
+
* **纯函数**:把"会话策略"折成"这次写盘真正要用的策略"。
|
|
5543
|
+
*
|
|
5544
|
+
* 为什么需要它(落点搬到 workspace 之外后的**关键一步**):`workspace-write` 的可写根是
|
|
5545
|
+
* **`policy.workspaceRoot`**,而会话策略的根是**会话 cwd**;落点
|
|
5546
|
+
* `<DSH_HOME>/storages/sandore/output/…` 永远在它之外 ⇒ 后端必然拒(`FS_SANDBOX_DENIED`)。
|
|
5547
|
+
* 而这次写盘的**委托人是应用本身**(把 Agent 输出落到 sandore 自己的数据目录),不是被沙箱约束的
|
|
5548
|
+
* 模型动作 ⇒ 该把**落点根**当作这次写盘的可写根。
|
|
5549
|
+
*
|
|
5550
|
+
* 逐档语义(**不对模式做任何放宽/收紧**):
|
|
5551
|
+
* - `read-only` ⇒ **原样返回**(用户的"只读"意图包括"别在我机器上新增文件",落盘照旧被拒);
|
|
5552
|
+
* - `workspace-write` ⇒ 原模式 + `workspaceRoot` 换成**落点根**(只把可写面从"会话工作区"
|
|
5553
|
+
* 换成"我们自己的数据目录",**不会**顺带放开工作区之外的任意路径);
|
|
5554
|
+
* - `danger-full-access` ⇒ **原样返回**(后端本来就不判);
|
|
5555
|
+
* - `undefined` ⇒ `undefined`(没有沙箱后端,本来就不判)。
|
|
5556
|
+
* @param policy - 会话策略({@link resolveWritePolicy} 的结果)。
|
|
5557
|
+
* @param outputRoot - 落点根({@link resolveOutputRoot} 的结果)。
|
|
5558
|
+
* @returns 这次写盘要传给 `writeText` 第 5 参的策略。
|
|
5559
|
+
*/
|
|
5560
|
+
function outputWritePolicy(policy, outputRoot) {
|
|
5561
|
+
if (policy === void 0 || policy.mode !== "workspace-write") return policy;
|
|
5562
|
+
return {
|
|
5563
|
+
mode: "workspace-write",
|
|
5564
|
+
workspaceRoot: outputRoot
|
|
5565
|
+
};
|
|
5566
|
+
}
|
|
5567
|
+
/**
|
|
5489
5568
|
* **纯函数**:这次失败是不是"沙箱拒绝写入"。
|
|
5490
5569
|
*
|
|
5491
5570
|
* 走 `code` 而不是 `instanceof FsError`:本模块**不依赖** `@deepseek-ai/dsh-fs`
|
|
@@ -5503,13 +5582,13 @@ function safeSegment(value) {
|
|
|
5503
5582
|
return cleaned === "" ? "step" : cleaned;
|
|
5504
5583
|
}
|
|
5505
5584
|
/**
|
|
5506
|
-
* **纯函数**:Agent
|
|
5507
|
-
* @param
|
|
5585
|
+
* **纯函数**:Agent 输出的落盘目录(`<落点根>/<instanceId>`)。
|
|
5586
|
+
* @param outputRoot - 落点根({@link resolveOutputRoot})。
|
|
5508
5587
|
* @param instanceId - 实例 id(≡ 宿主会话 id)。
|
|
5509
5588
|
* @returns 目录绝对路径(统一正斜杠;真正的绝对化由 `fs.resolve` 完成)。
|
|
5510
5589
|
*/
|
|
5511
|
-
function outputDirPath(
|
|
5512
|
-
return `${
|
|
5590
|
+
function outputDirPath(outputRoot, instanceId) {
|
|
5591
|
+
return `${outputRoot.replace(/\\/g, "/").replace(/\/+$/, "")}/${safeSegment(instanceId)}`;
|
|
5513
5592
|
}
|
|
5514
5593
|
/**
|
|
5515
5594
|
* **纯函数**:Agent 输出的文件名。
|
|
@@ -5522,19 +5601,22 @@ function outputFileName(stepId, attemptIndex) {
|
|
|
5522
5601
|
return `${safeSegment(stepId)}-${attempt}-output.md`;
|
|
5523
5602
|
}
|
|
5524
5603
|
/**
|
|
5525
|
-
*
|
|
5604
|
+
* 把一段文本落到实例的输出目录,返回**进程绝对路径**。
|
|
5526
5605
|
*
|
|
5527
5606
|
* 幂等:同一 `(instanceId, stepId, attemptIndex)` 重复调用覆盖同一文件(输出是快照语义,
|
|
5528
5607
|
* 不是追加日志)。
|
|
5608
|
+
*
|
|
5609
|
+
* **落点根由本函数自己解析**({@link resolveOutputRoot}):调用方**不需要也不该**传实例 cwd ——
|
|
5610
|
+
* 落点与工作区无关。
|
|
5529
5611
|
* @param fs - host 的 `ctx.fs` 接缝。
|
|
5530
|
-
* @param cwd - 会话工作目录(`state.cwd`)。
|
|
5531
5612
|
* @param input - `{instanceId, stepId, attemptIndex, text}`。
|
|
5532
|
-
* @param sandboxPolicy -
|
|
5533
|
-
*
|
|
5534
|
-
* @
|
|
5613
|
+
* @param sandboxPolicy - 这次写盘要用的策略(**已经过 {@link outputWritePolicy} 折算**的那个);
|
|
5614
|
+
* 不传 ⇒ 沙箱后端按部署兜底根判(P14 报障形态)。
|
|
5615
|
+
* @param rootOptions - 落点根解析入参(测试 / 非常规部署;缺省 = 生产三级解析)。
|
|
5616
|
+
* @returns 落点进程绝对路径(供 client 交给 better-sidebar / 应用内卡片打开)。
|
|
5535
5617
|
*/
|
|
5536
|
-
async function writeInstanceOutput(fs,
|
|
5537
|
-
const target = await fs.resolve(`${outputDirPath(
|
|
5618
|
+
async function writeInstanceOutput(fs, input, sandboxPolicy, rootOptions = {}) {
|
|
5619
|
+
const target = await fs.resolve(`${outputDirPath(resolveOutputRoot(rootOptions), input.instanceId)}/${outputFileName(input.stepId, input.attemptIndex)}`);
|
|
5538
5620
|
await fs.writeText(target, input.text, void 0, void 0, sandboxPolicy);
|
|
5539
5621
|
return fs.processPath(target);
|
|
5540
5622
|
}
|
|
@@ -5925,12 +6007,13 @@ const SendStepMessageInputSchema = z.object({
|
|
|
5925
6007
|
message: z.string().min(1)
|
|
5926
6008
|
});
|
|
5927
6009
|
/**
|
|
5928
|
-
* `writeInstanceOutputFile` 入参:把一段 **Agent 输出文本**
|
|
6010
|
+
* `writeInstanceOutputFile` 入参:把一段 **Agent 输出文本** 落到实例的输出目录。
|
|
5929
6011
|
*
|
|
5930
6012
|
* 落点规则由 host 唯一构造(`instance-output.ts`):
|
|
5931
|
-
* `<
|
|
5932
|
-
*
|
|
5933
|
-
*
|
|
6013
|
+
* `<DSH_HOME>/storages/sandore/output/<instanceId>/<stepId>-<attemptIndex>-output.md`
|
|
6014
|
+
* —— **与实例 cwd 无关**(不再往用户的工作区里写)。`attemptIndex` 区分同一 step 的多次执行段/
|
|
6015
|
+
* 重试(0-based);`<instanceId>/` 这一层防止多实例并发的同一 `stepId` 互相覆盖。
|
|
6016
|
+
* `text` 上限 1 MiB(输出是快照,不是日志)。
|
|
5934
6017
|
*/
|
|
5935
6018
|
const WriteInstanceOutputFileInputSchema = z.object({
|
|
5936
6019
|
instanceId: z.string(),
|
|
@@ -6794,44 +6877,46 @@ let SandoreRemote = (() => {
|
|
|
6794
6877
|
return await this.ctx.sandoreEngine.startInstance(request);
|
|
6795
6878
|
}
|
|
6796
6879
|
/**
|
|
6797
|
-
* 把一段 **Agent 输出文本**
|
|
6880
|
+
* 把一段 **Agent 输出文本** 落到实例的输出目录,返回落点绝对路径(需求 7)。
|
|
6798
6881
|
*
|
|
6799
6882
|
* 为什么需要这条 RPC:工作流导航树里 **result 叶子("Agent 输出")只有文本、没有文件**,
|
|
6800
|
-
* 而"点开文件"
|
|
6801
|
-
* `workspaceFence` 默认开启,越界路径打开只会得到一张 fence 报错,而部署侧没有开关能预关它)。
|
|
6883
|
+
* 而"点开文件"必须有文件。
|
|
6802
6884
|
*
|
|
6803
6885
|
* 落点(`<instanceId>/` 层**必需**:`stepId` 是定义内 id,多实例并发必然重名):
|
|
6804
|
-
* `<
|
|
6805
|
-
*
|
|
6886
|
+
* `<DSH_HOME>/storages/sandore/output/<instanceId>/<stepId>-<attemptIndex>-output.md`。
|
|
6887
|
+
* 该根**与实例 cwd 无关**(`instance-output.ts` 自己按 DSH home 三级解析),所以这里**不读
|
|
6888
|
+
* `state.cwd`**;写盘走 `ctx.fs`(本仓库 host 侧一律不直连 `node:fs`);**不主动清理**
|
|
6889
|
+
* (产出是用户的证据)。
|
|
6806
6890
|
*
|
|
6807
6891
|
* 快照优先取内存中的 live state(收尾中间态也能落),否则回退持久化快照;两者皆无抛
|
|
6808
|
-
* `NODE_NOT_FOUND
|
|
6892
|
+
* `NODE_NOT_FOUND`(取 state 只为**校验实例存在**,不再用于拼路径)。
|
|
6809
6893
|
*
|
|
6810
|
-
*
|
|
6811
|
-
*
|
|
6812
|
-
*
|
|
6813
|
-
*
|
|
6814
|
-
*
|
|
6815
|
-
*
|
|
6816
|
-
*
|
|
6894
|
+
* **沙箱纪律(两步,P14 报障的完整修复)**:写盘必须带**该实例宿主会话**的 per-call 沙箱策略
|
|
6895
|
+
* (`resolveWritePolicy`)—— `ctx.fs` 的沙箱后端在不给策略时按**部署兜底根**
|
|
6896
|
+
* (`config.workspaceRoot ?? process.cwd()`)判可写性。**落点搬到 workspace 之外后还要第二步**:
|
|
6897
|
+
* 用 `outputWritePolicy` 把策略的 `workspaceRoot` 折成**落点根**(`workspace-write` 的可写根就是
|
|
6898
|
+
* `policy.workspaceRoot`,不折就一定越界);`read-only` 原样保留 ⇒ 照旧被拒(用户的"只读"意图
|
|
6899
|
+
* 包括"别在我机器上新增文件")。被拒时**不再把内部错抛给 UI**:折成 `INVALID_OPERATION` +
|
|
6900
|
+
* 带落点路径的可见原因(client 侧据此回退到内存文本的应用内展示,见 `better-sidebar-open.ts`
|
|
6901
|
+
* 的 `write-failed`)。
|
|
6817
6902
|
*/
|
|
6818
6903
|
async writeInstanceOutputFile(input) {
|
|
6819
6904
|
const parsed = WriteInstanceOutputFileInputSchema.parse(input);
|
|
6820
|
-
|
|
6821
|
-
const state = live !== void 0 ? live : this.requireInstanceState(parsed.instanceId);
|
|
6905
|
+
if (this.ctx.sandoreEngine.getInstanceState(parsed.instanceId) === void 0) this.requireInstanceState(parsed.instanceId);
|
|
6822
6906
|
const services = this.ctx;
|
|
6907
|
+
const outputRoot = resolveOutputRoot({ ...typeof services.get("dshHomePath") === "function" ? { dshHomePath: services.get("dshHomePath") } : {} });
|
|
6823
6908
|
try {
|
|
6824
|
-
return { path: await writeInstanceOutput(this.ctx.fs,
|
|
6909
|
+
return { path: await writeInstanceOutput(this.ctx.fs, {
|
|
6825
6910
|
instanceId: parsed.instanceId,
|
|
6826
6911
|
stepId: parsed.stepId,
|
|
6827
6912
|
attemptIndex: parsed.attemptIndex,
|
|
6828
6913
|
text: parsed.text
|
|
6829
|
-
}, resolveWritePolicy({
|
|
6914
|
+
}, outputWritePolicy(resolveWritePolicy({
|
|
6830
6915
|
sandboxPolicy: services.get("sandboxPolicy"),
|
|
6831
6916
|
sessions: services.get("sessions")
|
|
6832
|
-
}, parsed.instanceId)) };
|
|
6917
|
+
}, parsed.instanceId), outputRoot), { explicitRoot: outputRoot }) };
|
|
6833
6918
|
} catch (error) {
|
|
6834
|
-
if (isSandboxDenial(error)) throw new SandoreError(`cannot write the step output into "${outputDirPath(
|
|
6919
|
+
if (isSandboxDenial(error)) throw new SandoreError(`cannot write the step output into "${outputDirPath(outputRoot, parsed.instanceId)}": the file sandbox of this instance session denies that location`, "INVALID_OPERATION");
|
|
6835
6920
|
throw error;
|
|
6836
6921
|
}
|
|
6837
6922
|
}
|
|
@@ -172,13 +172,13 @@ export declare const INSTANCE_LIST_FRAGMENTS: {
|
|
|
172
172
|
};
|
|
173
173
|
status: ArgumentFragment & {
|
|
174
174
|
readonly zod: z.ZodEnum<{
|
|
175
|
-
|
|
175
|
+
pending: "pending";
|
|
176
176
|
running: "running";
|
|
177
|
+
interacting: "interacting";
|
|
177
178
|
completed: "completed";
|
|
178
179
|
failed: "failed";
|
|
179
|
-
paused: "paused";
|
|
180
|
-
pending: "pending";
|
|
181
180
|
aborted: "aborted";
|
|
181
|
+
paused: "paused";
|
|
182
182
|
}>;
|
|
183
183
|
};
|
|
184
184
|
limit: ArgumentFragment & {
|
|
@@ -221,8 +221,8 @@ export declare const SINGLE_TOOL_FRAGMENTS: {
|
|
|
221
221
|
detail: ArgumentFragment & {
|
|
222
222
|
readonly zod: z.ZodEnum<{
|
|
223
223
|
steps: "steps";
|
|
224
|
-
messages: "messages";
|
|
225
224
|
step: "step";
|
|
225
|
+
messages: "messages";
|
|
226
226
|
summary: "summary";
|
|
227
227
|
}>;
|
|
228
228
|
};
|
|
@@ -342,8 +342,8 @@ export declare const VisualizeArgsSchema: z.ZodObject<{
|
|
|
342
342
|
export declare const StatusArgsSchema: z.ZodObject<{
|
|
343
343
|
detail: z.ZodType<unknown, unknown, z.core.$ZodTypeInternals<unknown, unknown>> & z.ZodEnum<{
|
|
344
344
|
steps: "steps";
|
|
345
|
-
messages: "messages";
|
|
346
345
|
step: "step";
|
|
346
|
+
messages: "messages";
|
|
347
347
|
summary: "summary";
|
|
348
348
|
}>;
|
|
349
349
|
instanceId: z.ZodType<unknown, unknown, z.core.$ZodTypeInternals<unknown, unknown>> & z.ZodString;
|
package/lib/workflow/domain.d.ts
CHANGED
|
@@ -18,14 +18,14 @@ export declare const sandoreDomainSpec: {
|
|
|
18
18
|
taskId: string;
|
|
19
19
|
workflowId: string;
|
|
20
20
|
cwd: string;
|
|
21
|
-
status: "
|
|
21
|
+
status: "pending" | "running" | "interacting" | "completed" | "failed" | "aborted" | "paused";
|
|
22
22
|
originalInput: string;
|
|
23
23
|
currentStageId: string;
|
|
24
24
|
executedSteps: {
|
|
25
25
|
id: string;
|
|
26
26
|
stageId: string;
|
|
27
27
|
name: string;
|
|
28
|
-
status: "
|
|
28
|
+
status: "running" | "interacting" | "completed" | "failed" | "paused" | "blocked" | "ready";
|
|
29
29
|
visitCount: number;
|
|
30
30
|
stepId?: string | undefined;
|
|
31
31
|
command?: {
|
|
@@ -78,7 +78,7 @@ export declare const sandoreDomainSpec: {
|
|
|
78
78
|
}[] | undefined;
|
|
79
79
|
sessionMeta?: {
|
|
80
80
|
synthetic: boolean;
|
|
81
|
-
command: "
|
|
81
|
+
command: "shell" | "agent" | "prompt" | "skill" | "mcp";
|
|
82
82
|
childId?: string | undefined;
|
|
83
83
|
hostSessionId?: string | undefined;
|
|
84
84
|
} | undefined;
|
|
@@ -181,7 +181,7 @@ export declare const sandoreDomainSpec: {
|
|
|
181
181
|
error?: string | undefined;
|
|
182
182
|
pausedAt?: string | undefined;
|
|
183
183
|
sessionMessages?: {
|
|
184
|
-
type: "text" | "
|
|
184
|
+
type: "text" | "tool" | "reasoning";
|
|
185
185
|
text?: string | undefined;
|
|
186
186
|
tool?: string | undefined;
|
|
187
187
|
args?: unknown;
|
|
@@ -213,7 +213,7 @@ export declare const sandoreDomainSpec: {
|
|
|
213
213
|
stageInfos: {
|
|
214
214
|
id: string;
|
|
215
215
|
name: string;
|
|
216
|
-
status: "
|
|
216
|
+
status: "running" | "interacting" | "completed" | "failed" | "paused" | "blocked" | "ready";
|
|
217
217
|
}[];
|
|
218
218
|
pendingInteractions: ({
|
|
219
219
|
id: string;
|
|
@@ -268,7 +268,7 @@ export declare const sandoreDomainSpec: {
|
|
|
268
268
|
stageContracts: Record<string, {
|
|
269
269
|
stageId: string;
|
|
270
270
|
stageName: string;
|
|
271
|
-
status: "
|
|
271
|
+
status: "running" | "interacting" | "completed" | "failed" | "paused" | "blocked" | "ready";
|
|
272
272
|
artifacts: {
|
|
273
273
|
stepId: string;
|
|
274
274
|
outputToId: string;
|
|
@@ -298,7 +298,7 @@ export declare const sandoreDomainSpec: {
|
|
|
298
298
|
permission?: string | undefined;
|
|
299
299
|
userMessages?: {
|
|
300
300
|
id: string;
|
|
301
|
-
kind: "
|
|
301
|
+
kind: "message" | "input" | "answer" | "resume" | "retry";
|
|
302
302
|
target: string;
|
|
303
303
|
content: string;
|
|
304
304
|
createdAt: string;
|