@meiyukichan/dsh-sandore 0.1.2-rc.2 → 0.1.2-rc.4
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/cordis.patch.yml +21 -13
- package/lib/client/DagEditor.d.ts +23 -0
- package/lib/client/DagEditor.js +11 -2
- package/lib/client/FlowView.d.ts +19 -0
- package/lib/client/FlowView.js +31 -3
- package/lib/client/better-sidebar-open.d.ts +28 -3
- package/lib/client/better-sidebar-open.js +47 -15
- package/lib/client/dag-editor-model.d.ts +8 -1
- package/lib/client/dag-editor-model.js +10 -3
- package/lib/client/definition-flow-open.d.ts +39 -1
- package/lib/client/definition-flow-open.js +79 -1
- package/lib/client/definition-flow-refresh.d.ts +48 -0
- package/lib/client/definition-flow-refresh.js +12 -0
- package/lib/client/definition-flow-source.d.ts +57 -10
- package/lib/client/definition-flow-source.js +60 -9
- package/lib/client/definition-flow-step-modal.d.ts +97 -0
- package/lib/client/definition-flow-step-modal.js +174 -0
- package/lib/client/definition-flow-tab.d.ts +31 -3
- package/lib/client/definition-flow-tab.js +113 -4
- package/lib/client/definition-flow-tool-card.d.ts +16 -0
- package/lib/client/definition-flow-tool-card.js +36 -2
- package/lib/client/definition-flow-view.d.ts +58 -3
- package/lib/client/definition-flow-view.js +106 -10
- package/lib/client/definition-flow-watch.d.ts +49 -0
- package/lib/client/definition-flow-watch.js +162 -0
- package/lib/client/flow-layout.d.ts +11 -0
- package/lib/client/flow-layout.js +50 -10
- package/lib/client/flow-render.js +19 -1
- package/lib/client/index.js +16 -4
- package/lib/client/locales/en.d.ts +10 -0
- package/lib/client/locales/en.js +15 -0
- package/lib/client/locales/zh.d.ts +10 -0
- package/lib/client/locales/zh.js +15 -0
- package/lib/client.js +24417 -23559
- package/lib/index.js +502 -110
- package/lib/microflow/branch-evaluator.js +8 -1
- package/lib/microflow/finalize-manager.js +8 -1
- package/lib/microflow/fork-session.d.ts +6 -1
- package/lib/microflow/fork-session.js +48 -1
- package/lib/microflow/step-navigator.d.ts +8 -2
- package/lib/microflow/step-navigator.js +8 -2
- package/lib/workflow/capability-registry.d.ts +5 -5
- package/lib/workflow/capability-registry.js +15 -7
- package/lib/workflow/dag-navigator.d.ts +8 -0
- package/lib/workflow/dag-navigator.js +8 -0
- package/lib/workflow/definition-events.d.ts +58 -0
- package/lib/workflow/definition-events.js +51 -0
- package/lib/workflow/domain.d.ts +30 -11
- package/lib/workflow/domain.js +60 -11
- package/lib/workflow/instance-output.d.ts +102 -51
- package/lib/workflow/instance-output.js +158 -55
- package/lib/workflow/list-instances-tool.d.ts +3 -3
- package/lib/workflow/manager-tool.js +190 -7
- package/lib/workflow/persistence.d.ts +1 -1
- package/lib/workflow/persistence.js +22 -2
- package/lib/workflow/remote.d.ts +15 -13
- package/lib/workflow/remote.js +27 -19
- package/lib/workflow/seam.d.ts +19 -0
- package/lib/workflow/sse.js +3 -0
- package/lib/workflow/status-tool.d.ts +1 -1
- package/lib/workflow/types.d.ts +2 -2
- package/lib/workflow/wire.d.ts +5 -4
- package/lib/workflow/wire.js +5 -4
- package/package.json +1 -1
- package/packages/client/DagEditor.tsx +12 -1
- package/packages/client/FlowView.tsx +45 -4
- package/packages/client/better-sidebar-open.spec.ts +99 -25
- package/packages/client/better-sidebar-open.ts +58 -18
- package/packages/client/dag-editor-model.spec.ts +6 -4
- package/packages/client/dag-editor-model.ts +10 -3
- package/packages/client/definition-flow-interaction.spec.ts +331 -0
- package/packages/client/definition-flow-open.spec.ts +59 -0
- package/packages/client/definition-flow-open.ts +82 -1
- package/packages/client/definition-flow-refresh.module.css +53 -0
- package/packages/client/definition-flow-refresh.tsx +71 -0
- package/packages/client/definition-flow-source.spec.ts +53 -6
- package/packages/client/definition-flow-source.ts +80 -11
- package/packages/client/definition-flow-step-modal.module.css +53 -0
- package/packages/client/definition-flow-step-modal.tsx +272 -0
- package/packages/client/definition-flow-tab.module.css +14 -0
- package/packages/client/definition-flow-tab.spec.ts +273 -2
- package/packages/client/definition-flow-tab.tsx +125 -6
- package/packages/client/definition-flow-tool-card.module.css +6 -0
- package/packages/client/definition-flow-tool-card.spec.ts +291 -1
- package/packages/client/definition-flow-tool-card.tsx +63 -1
- package/packages/client/definition-flow-view.tsx +131 -10
- package/packages/client/definition-flow-watch.spec.ts +143 -0
- package/packages/client/definition-flow-watch.ts +165 -0
- package/packages/client/flow-layout.spec.ts +56 -4
- package/packages/client/flow-layout.ts +50 -10
- package/packages/client/flow-render.spec.ts +49 -0
- package/packages/client/flow-render.ts +19 -1
- package/packages/client/flow-view.module.css +10 -0
- package/packages/client/index.ts +17 -4
- package/packages/client/locales/en.ts +15 -0
- package/packages/client/locales/zh.ts +15 -0
- package/packages/client/locales.spec.ts +6 -0
- package/packages/client/orchestration-tool-card.spec.ts +8 -2
- package/packages/client/orchestration-tool-view.spec.ts +6 -0
- package/packages/client/run-view-host-filter.spec.ts +5 -2
- package/packages/client/step-view/tool/tool-call-view.spec.ts +2 -0
- package/packages/client/tooltip-usage.spec.ts +3 -2
- package/packages/microflow/branch-evaluator.ts +8 -1
- package/packages/microflow/finalize-manager.spec.ts +53 -4
- package/packages/microflow/finalize-manager.ts +8 -1
- package/packages/microflow/fork-session.spec.ts +93 -4
- package/packages/microflow/fork-session.ts +56 -2
- package/packages/microflow/step-navigator.ts +8 -2
- package/packages/resources/README.md +78 -6
- package/packages/resources/skills/sandore-workflow-authoring/SKILL.md +289 -187
- package/packages/resources/skills/sandore-workflow-authoring/reference/authoring-microflow.md +178 -48
- package/packages/resources/skills/sandore-workflow-authoring/reference/authoring-step.md +151 -32
- package/packages/resources/skills/sandore-workflow-authoring/reference/authoring-workflow.md +369 -80
- package/packages/resources/skills/sandore-workflow-authoring/reference/global-declaration-surface.md +42 -0
- package/packages/resources/skills/sandore-workflow-authoring/reference/index.md +41 -12
- package/packages/resources/skills/sandore-workflow-authoring/reference/pitfalls.md +108 -0
- package/packages/resources/skills/sandore-workflow-authoring/reference/skeleton-minimal.yaml +25 -0
- package/packages/resources/skills/sandore-workflow-authoring/reference/template-microflow-entry.yaml +21 -15
- package/packages/resources/skills/sandore-workflow-authoring/reference/template-workflow.yaml +358 -138
- package/packages/resources/skills/sandore-workflow-authoring/reference/verify-by-running.md +245 -0
- package/packages/resources/skills/sandore-workflow-authoring/reference/workflow-yaml-spec.md +196 -58
- package/packages/workflow/capability-registry.spec.ts +25 -0
- package/packages/workflow/capability-registry.ts +15 -7
- package/packages/workflow/dag-navigator.ts +8 -0
- package/packages/workflow/definition-events.spec.ts +367 -0
- package/packages/workflow/definition-events.ts +102 -0
- package/packages/workflow/domain-persistence.spec.ts +242 -0
- package/packages/workflow/domain.ts +64 -11
- package/packages/workflow/instance-output.spec.ts +144 -29
- package/packages/workflow/instance-output.ts +180 -55
- package/packages/workflow/manager-tool.spec.ts +263 -2
- package/packages/workflow/manager-tool.ts +192 -7
- package/packages/workflow/persistence.ts +21 -2
- package/packages/workflow/remote.spec.ts +58 -20
- package/packages/workflow/remote.ts +27 -19
- package/packages/workflow/schema.spec.ts +18 -16
- package/packages/workflow/seam.ts +19 -0
- package/packages/workflow/skill-resource-lock.spec.ts +1301 -21
- package/packages/workflow/sse.ts +3 -0
- package/packages/workflow/types.ts +2 -2
- package/packages/workflow/wire.ts +5 -4
- package/packages/tests/fixtures/workflow-authoring-template.ts +0 -303
package/cordis.patch.yml
CHANGED
|
@@ -5,8 +5,13 @@
|
|
|
5
5
|
# dsh plugin --profile <name> add @meiyukichan/dsh-sandore
|
|
6
6
|
#
|
|
7
7
|
# 命令会把 `dsh-sandore` 追加进 profile 的 `dsh.profile.bundles` 层栈;启动时
|
|
8
|
-
# profile boot 合并本 patch(单条 `insert`
|
|
9
|
-
#
|
|
8
|
+
# profile boot 合并本 patch(单条 `insert` 插件行),无需手改任何 profile 文件。
|
|
9
|
+
# 一个命令完成安装与挂载(host 引擎 + client UI + 文件介质持久化)。
|
|
10
|
+
#
|
|
11
|
+
# ⚠️ **本文件与仓库根的 `cordis.yml`(本地 `--patch` overlay)必须同步**:两者都是
|
|
12
|
+
# 「插件行 + storage 配置」的同构层,e2e 进程吃的是**本文件**(临时 profile 走 bundle
|
|
13
|
+
# 通道),本地 dev 吃 `cordis.yml`。历史上两处漂过(`cordis.yml` 改成 json 介质而本文件
|
|
14
|
+
# 仍把 `sandore` 路由到 sqlite ⇒ 真机 e2e 里介质没换、还在写 sqlite)。
|
|
10
15
|
#
|
|
11
16
|
# 行 id 用语义前缀 `sandore`:DSH bundle 的行 id 都是语义前缀(ui-* / tool-* /
|
|
12
17
|
# storage-* 等),`sandore` 唯一且不易与其它 overlay 的 id patch 冲突。
|
|
@@ -14,10 +19,12 @@
|
|
|
14
19
|
# 插件行 name = 本包名('@meiyukichan/dsh-sandore'):host 半由 main 提供;client 半经
|
|
15
20
|
# package.json 的 `dsh.client` 声明被 client-modules 扫描发现(exports["./client"])。
|
|
16
21
|
#
|
|
17
|
-
#
|
|
18
|
-
#
|
|
19
|
-
#
|
|
20
|
-
#
|
|
22
|
+
# ## sandore 存储介质 = DSH `storage-json` 的 `per-record` 布局(本批从 sqlite 迁出)
|
|
23
|
+
# 落点 `~/.dsh/storages/sandore/{definitions,microflows,state}/<id>.json`(一条记录一个文件)。
|
|
24
|
+
# 路由走 **base bundle 的默认 backend(= `json`)** ⇒ 这里**刻意不再有 `storage-domain` 的
|
|
25
|
+
# `routes: {sandore: sqlite}` 那一行**(判据见 `storage-domain/src/index.ts:109` 的
|
|
26
|
+
# `routes?.[name] ?? config.backend`)。**回滚到 sqlite** = 把下面注释掉的 `storage-domain`
|
|
27
|
+
# 段加回来(`storage-sqlite` 行仍插着;sqlite 后端**完全不看 `layout` 与记录戳**,两套介质可并存)。
|
|
21
28
|
|
|
22
29
|
- insert:
|
|
23
30
|
- id: sandore
|
|
@@ -25,15 +32,16 @@
|
|
|
25
32
|
config:
|
|
26
33
|
defaultTimeoutMs: 7200000
|
|
27
34
|
maxConcurrentSteps: 0
|
|
35
|
+
# sqlite 后端仍挂着(回滚用);sandore 域当前**不路由**到它。
|
|
36
|
+
# storage-sqlite 包随本包 dependencies 安装(^0.1.2-rc.1,与宿主 rc.1 对齐),profile 可直接解析。
|
|
28
37
|
- id: storage-sqlite
|
|
29
38
|
name: '@deepseek-ai/dsh-storage-sqlite'
|
|
30
39
|
config:
|
|
31
40
|
path: !!js dshHomePath('storages/sandore.sqlite')
|
|
32
41
|
|
|
33
|
-
#
|
|
34
|
-
#
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
sandore: sqlite
|
|
42
|
+
# 回滚到 sqlite 介质时,把下面这段解除注释(整行替换 storage-domain 的 config):
|
|
43
|
+
# - id: storage-domain
|
|
44
|
+
# config:
|
|
45
|
+
# backend: json
|
|
46
|
+
# routes:
|
|
47
|
+
# sandore: sqlite
|
|
@@ -2,6 +2,8 @@ import '@xyflow/react/dist/style.css';
|
|
|
2
2
|
import './reactflow-theme.css';
|
|
3
3
|
import type { TranslateNS } from '@deepseek-ai/dsh-client-ui-slots';
|
|
4
4
|
import type { MicroflowDefinition, MicroflowRecord } from '@meiyukichan/dsh-sandore/schema';
|
|
5
|
+
import { type MergedStep } from './dag-editor-model.ts';
|
|
6
|
+
import { type FlowTarget } from './FlowEditor.tsx';
|
|
5
7
|
import { type SaveDefinition } from './sandore-save.ts';
|
|
6
8
|
import type { WorkflowOverlayFace } from './workflow-face.ts';
|
|
7
9
|
/**
|
|
@@ -84,5 +86,26 @@ export declare function MicroflowDescriptionField({ microflow, onChange, t }: {
|
|
|
84
86
|
readonly onChange: (next: MicroflowDefinition) => void;
|
|
85
87
|
readonly t: TranslateNS<'sandore'>;
|
|
86
88
|
}): import("react").JSX.Element;
|
|
89
|
+
/** Step 完整字段编辑器(标题 + 名称行 + 执行 command + 流控 FlowEditor + 映射 + gates)。 */
|
|
90
|
+
/**
|
|
91
|
+
* 单个 step 的**详情编辑器**(编排页右栏;F6 起也被「定义流程图」的步骤弹窗复用)。
|
|
92
|
+
*
|
|
93
|
+
* ⚠️ **导出它是有意的**:`definition-flow-step-modal.tsx` 要给出与编排页**逐字段一致**的详情
|
|
94
|
+
* (用户口径:「内容就是工作流编排那里,点击某个 step 后右边的详情的内容」)⇒ 只允许**同一个组件**
|
|
95
|
+
* 被两处使用,绝不允许在流程弹窗里另写一套「长得像」的表单(那必然漂移)。
|
|
96
|
+
* 因此它的 props 保持**纯模型 + 回调**(`step` / `stageSteps` / `targets` / `ruleKeys` /
|
|
97
|
+
* `onChange` / `onRemove`),不带任何 DagEditor 内部状态 ⇒ 弹窗侧可以从「读回来的定义」直接组装。
|
|
98
|
+
*/
|
|
99
|
+
export declare function StepEditor({ step, stageSteps, targets, ruleKeys, onChange, onRemove, t }: {
|
|
100
|
+
readonly step: MergedStep;
|
|
101
|
+
/** 当前 Stage 内全部合并 step(含 branches),用于计算回环环上节点。 */
|
|
102
|
+
readonly stageSteps: readonly MergedStep[];
|
|
103
|
+
readonly targets: readonly FlowTarget[];
|
|
104
|
+
/** 工作流级 rules 内容表的键集合(供 step.rules 键引用编辑的勾选项)。 */
|
|
105
|
+
readonly ruleKeys: readonly string[];
|
|
106
|
+
readonly onChange: (step: MergedStep) => void;
|
|
107
|
+
readonly onRemove: () => void;
|
|
108
|
+
readonly t: TranslateNS<'sandore'>;
|
|
109
|
+
}): import("react").JSX.Element;
|
|
87
110
|
/** 渲染 DagEditor(两层 DAG 画布 + 编辑侧栏 + YAML 预览)。 */
|
|
88
111
|
export declare function DagEditor({ t, initialYaml, initialId, save, onSaved, face, libraryEntry, readonly, setReadonly }: DagEditorProps): import("react").JSX.Element;
|
package/lib/client/DagEditor.js
CHANGED
|
@@ -396,7 +396,16 @@ function OutputRow({ item, onChange, onRemove, t }) {
|
|
|
396
396
|
}, placeholder: mode === 'path' ? t('editor.field.relativePath') : t('editor.field.regex') }), _jsx(Button, { variant: "ghost", size: "sm", icon: _jsx(IconTrashOutline16, {}), onClick: onRemove, "aria-label": t('editor.remove.item') })] }));
|
|
397
397
|
}
|
|
398
398
|
/** Step 完整字段编辑器(标题 + 名称行 + 执行 command + 流控 FlowEditor + 映射 + gates)。 */
|
|
399
|
-
|
|
399
|
+
/**
|
|
400
|
+
* 单个 step 的**详情编辑器**(编排页右栏;F6 起也被「定义流程图」的步骤弹窗复用)。
|
|
401
|
+
*
|
|
402
|
+
* ⚠️ **导出它是有意的**:`definition-flow-step-modal.tsx` 要给出与编排页**逐字段一致**的详情
|
|
403
|
+
* (用户口径:「内容就是工作流编排那里,点击某个 step 后右边的详情的内容」)⇒ 只允许**同一个组件**
|
|
404
|
+
* 被两处使用,绝不允许在流程弹窗里另写一套「长得像」的表单(那必然漂移)。
|
|
405
|
+
* 因此它的 props 保持**纯模型 + 回调**(`step` / `stageSteps` / `targets` / `ruleKeys` /
|
|
406
|
+
* `onChange` / `onRemove`),不带任何 DagEditor 内部状态 ⇒ 弹窗侧可以从「读回来的定义」直接组装。
|
|
407
|
+
*/
|
|
408
|
+
export function StepEditor({ step, stageSteps, targets, ruleKeys, onChange, onRemove, t }) {
|
|
400
409
|
const inputFrom = step.inputFrom ?? [];
|
|
401
410
|
const outputTo = step.outputTo ?? [];
|
|
402
411
|
const passThroughVars = step.passThroughVars ?? [];
|
|
@@ -966,7 +975,7 @@ export function DagEditor({ t, initialYaml, initialId, save, onSaved, face, libr
|
|
|
966
975
|
return (_jsxs("section", { className: css.editor, "aria-label": t('editor.title'), "data-sandore-dag-editor": true, ...(library === undefined ? {} : { 'data-sandore-library-entry': library.id }), children: [_jsxs("header", { className: css.header, children: [_jsx(Tooltip, { label: library?.record.name ?? definition.workflow.name, side: "bottom", delayMs: 500, children: _jsx("span", { className: css.title, children: library?.record.name ?? definition.workflow.name }) }), _jsxs("span", { className: css.headerActions, "data-sandore-top-actions": true, children: [library === undefined ? (_jsxs(_Fragment, { children: [_jsx(Button, { size: "sm", variant: "ghost", icon: _jsx(EyeIcon, {}), onClick: () => { setPreviewOpen(current => !current); }, children: t('editor.preview') }), _jsx(Button, { size: "sm", variant: "ghost", icon: _jsx(IconDownloadOutline16, {}), onClick: exportYaml, children: t('editor.export') }), _jsx(Button, { size: "sm", variant: "ghost", icon: _jsx(IconCopyOutline16, {}), onClick: () => { setImportOpen(true); }, children: t('editor.import') }), _jsx(Button, { size: "sm", variant: "primary", icon: _jsx(IconSparkle16, {}), onClick: () => { setToast(current => ({ seq: (current?.seq ?? 0) + 1, message: t('editor.chatCreate.hint') })); }, children: t('editor.chatCreate') }), id !== undefined && (_jsx(Button, { size: "sm", variant: "primary", icon: _jsx(IconPlayOutline16, {}), onClick: runTest, children: t('editor.test') }))] })) : (_jsx(Button, { size: "sm", variant: "ghost", icon: _jsx(IconCopyOutline16, {}), "data-sandore-library-entry-edit-yaml": true, onClick: library.onEditYaml, children: t('library.edit.yaml') })), _jsx(Button, { size: "sm", variant: "primary", icon: _jsx(IconCheckOutline16, {}),
|
|
967
976
|
/* 库条目**不受只读锁约束**(只读锁的是 workflow id)⇒ 库模式的保存按钮不看 `readonly`;
|
|
968
977
|
工作流模式逐字保留原判据。 */
|
|
969
|
-
disabled: saveState.kind === 'pending' || (library === undefined && readonly), onClick: () => { void runSave(); }, children: saveState.kind === 'pending' ? t('editor.saving') : t('save') })] })] }), saveState.kind === 'error' && (_jsx("p", { className: css.error, role: "alert", children: saveState.details === undefined
|
|
978
|
+
disabled: saveState.kind === 'pending' || (library === undefined && readonly), "data-sandore-dag-save": true, onClick: () => { void runSave(); }, children: saveState.kind === 'pending' ? t('editor.saving') : t('save') })] })] }), saveState.kind === 'error' && (_jsx("p", { className: css.error, role: "alert", children: saveState.details === undefined
|
|
970
979
|
? t('editor.save.failed', { message: saveState.message })
|
|
971
980
|
: t('editor.save.failed', { message: `${saveState.message} — ${saveState.details}` }) })), _jsxs("div", { className: css.canvasRow, children: [_jsxs("div", { className: css.canvas, children: [_jsxs("div", { className: css.canvasTabs, children: [drillMicroflowId !== undefined && !libraryMode && (_jsxs("button", { type: "button", className: css.backBtn, onClick: backToStages, children: ["\u2190 ", t('editor.microflowDag')] })), _jsx("span", { className: css.crumb, children: drillMicroflowId === undefined
|
|
972
981
|
? t('editor.microflowDag')
|
package/lib/client/FlowView.d.ts
CHANGED
|
@@ -63,6 +63,13 @@ export interface FlowViewProps {
|
|
|
63
63
|
readonly width?: number | undefined;
|
|
64
64
|
/** 无障碍标签(缺省用 `workflowId`;不编文案)。 */
|
|
65
65
|
readonly label?: string | undefined;
|
|
66
|
+
/**
|
|
67
|
+
* **点击步骤卡片**(F6:弹窗展示/编辑该 step 的详情)。
|
|
68
|
+
*
|
|
69
|
+
* 给了它就启用事件委托(`closest('[data-flow-step]')`),并把卡片的
|
|
70
|
+
* `(microflowId, stepId)` 回调出去;**不给 ⇒ 卡片不可点、DOM 与行为逐字不变**。
|
|
71
|
+
*/
|
|
72
|
+
readonly onStepOpen?: ((target: FlowStepClickTarget) => void) | undefined;
|
|
66
73
|
/**
|
|
67
74
|
* 文案函数(B4 接线后由调用方给;**缺省仍渲原因码**)。
|
|
68
75
|
*
|
|
@@ -71,6 +78,18 @@ export interface FlowViewProps {
|
|
|
71
78
|
*/
|
|
72
79
|
readonly t?: ((key: FlowNoticeKey) => string) | undefined;
|
|
73
80
|
}
|
|
81
|
+
/** 被点开的步骤卡片身份(`microflowId` 可能为空串 = 卡片没带容器身份,调用方自行判空)。 */
|
|
82
|
+
export interface FlowStepClickTarget {
|
|
83
|
+
readonly microflowId: string;
|
|
84
|
+
readonly stepId: string;
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* 从点击目标上读卡片身份(**事件委托**:卡片是注入进去的字符串、挂不了 React 处理器
|
|
88
|
+
* ⇒ 只能在 `<svg>` 这一层代理;`theme-tokens.spec.ts` 那条"只许对 inner 用注入"的判据仍然成立)。
|
|
89
|
+
* @param target - 事件目标(`null` 安全)。
|
|
90
|
+
* @returns 卡片身份;点的不是卡片 ⇒ undefined。
|
|
91
|
+
*/
|
|
92
|
+
export declare function flowStepTargetOf(target: EventTarget | null): FlowStepClickTarget | undefined;
|
|
74
93
|
/**
|
|
75
94
|
* 同步链路:yaml → 图 → 计划 → SVG(**永不抛**,失败折成 `error` 态)。
|
|
76
95
|
* @param yaml - 定义 yaml。
|
package/lib/client/FlowView.js
CHANGED
|
@@ -26,7 +26,7 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
|
26
26
|
*/
|
|
27
27
|
import { useEffect, useMemo, useRef, useState } from 'react';
|
|
28
28
|
import { flowGraphOfYaml } from "./flow-model.js";
|
|
29
|
-
import { FLOW_MIN_WIDTH, flowWidthTier, planFlow } from "./flow-layout.js";
|
|
29
|
+
import { FLOW_MAX_DISPLAY_WIDTH, FLOW_MIN_WIDTH, flowWidthTier, planFlow } from "./flow-layout.js";
|
|
30
30
|
import { renderFlowSvg } from "./flow-render.js";
|
|
31
31
|
import css from './flow-view.module.css';
|
|
32
32
|
/** 原因码 → 文案键(唯一映射表;未知/新增的原因码由类型系统兜住)。 */
|
|
@@ -42,6 +42,26 @@ export function flowNoticeKeyOf(reason) {
|
|
|
42
42
|
return 'definitionFlow.error.noChannel';
|
|
43
43
|
}
|
|
44
44
|
}
|
|
45
|
+
/**
|
|
46
|
+
* 从点击目标上读卡片身份(**事件委托**:卡片是注入进去的字符串、挂不了 React 处理器
|
|
47
|
+
* ⇒ 只能在 `<svg>` 这一层代理;`theme-tokens.spec.ts` 那条"只许对 inner 用注入"的判据仍然成立)。
|
|
48
|
+
* @param target - 事件目标(`null` 安全)。
|
|
49
|
+
* @returns 卡片身份;点的不是卡片 ⇒ undefined。
|
|
50
|
+
*/
|
|
51
|
+
export function flowStepTargetOf(target) {
|
|
52
|
+
if (target === null || typeof target !== 'object')
|
|
53
|
+
return undefined;
|
|
54
|
+
const element = target;
|
|
55
|
+
if (typeof element.closest !== 'function')
|
|
56
|
+
return undefined;
|
|
57
|
+
const card = element.closest('[data-flow-step]');
|
|
58
|
+
if (card === null)
|
|
59
|
+
return undefined;
|
|
60
|
+
const stepId = card.getAttribute('data-flow-step') ?? '';
|
|
61
|
+
if (stepId === '')
|
|
62
|
+
return undefined;
|
|
63
|
+
return { microflowId: card.getAttribute('data-flow-microflow') ?? '', stepId };
|
|
64
|
+
}
|
|
45
65
|
/**
|
|
46
66
|
* 同步链路:yaml → 图 → 计划 → SVG(**永不抛**,失败折成 `error` 态)。
|
|
47
67
|
* @param yaml - 定义 yaml。
|
|
@@ -95,13 +115,21 @@ export function useMeasuredWidth() {
|
|
|
95
115
|
export function FlowView(props) {
|
|
96
116
|
const [rootRef, measured] = useMeasuredWidth();
|
|
97
117
|
const explicit = props.width !== undefined && Number.isFinite(props.width) ? Math.max(FLOW_MIN_WIDTH, props.width) : undefined;
|
|
98
|
-
|
|
118
|
+
// F5:**渲染宽度封顶 400**(定稿页 `final.html` 的 `figure{width:400px}`)——宽容器只多留白,不把图拉宽。
|
|
119
|
+
const tier = Math.min(explicit ?? flowWidthTier(measured), FLOW_MAX_DISPLAY_WIDTH);
|
|
99
120
|
const state = useFlowViewState(props.yaml, props.workflowId ?? '', tier);
|
|
100
121
|
const workflowId = props.workflowId ?? (state.state === 'ready' ? state.plan.header.name : '');
|
|
101
122
|
const label = props.label ?? props.workflowId;
|
|
102
123
|
const notices = state.state === 'ready' ? state.plan.notes : [];
|
|
124
|
+
const onStepOpen = props.onStepOpen;
|
|
103
125
|
// B4:给了文案函数就渲文案,没给仍渲原因码(B3 的行为逐字不变)。
|
|
104
126
|
const textOf = (key, fallback) => props.t === undefined ? fallback : props.t(key);
|
|
105
|
-
return (_jsxs("div", { className: css.root, ref: rootRef, "data-sandore-definition-flow": workflowId, "data-definition-flow-state": state.state, "data-definition-flow-width": tier, children: [_jsx("div", { className: css.scroll, children: _jsx("div", { className: css.canvas, style: {
|
|
127
|
+
return (_jsxs("div", { className: css.root, ref: rootRef, "data-sandore-definition-flow": workflowId, "data-definition-flow-state": state.state, "data-definition-flow-width": tier, ...(onStepOpen === undefined ? {} : { 'data-definition-flow-clickable': '' }), children: [_jsx("div", { className: css.scroll, children: _jsx("div", { className: css.canvas, "data-definition-flow-canvas": "", style: { width: tier }, children: state.state === 'ready' ? (_jsx("svg", { className: css.svg, viewBox: state.svg.viewBox, width: "100%", role: "img", xmlns: "http://www.w3.org/2000/svg", "data-definition-flow-svg": "", ...(onStepOpen === undefined
|
|
128
|
+
? {}
|
|
129
|
+
: { onClick: (event) => {
|
|
130
|
+
const hit = flowStepTargetOf(event.target);
|
|
131
|
+
if (hit !== undefined)
|
|
132
|
+
onStepOpen(hit);
|
|
133
|
+
} }), ...(label === undefined ? {} : { 'aria-label': label }), dangerouslySetInnerHTML: { __html: state.svg.inner } })) : null }) }), state.state === 'error' ? (_jsx("div", { className: `${css.notice} ${css.noticeError}`, "data-definition-flow-notice": "", "data-definition-flow-reason": state.reason, role: "alert", children: [textOf(flowNoticeKeyOf(state.reason), state.reason), state.message]
|
|
106
134
|
.filter(part => part !== undefined && part !== '').join(' · ') })) : null, state.state === 'loading' ? (_jsx("div", { className: css.notice, "data-definition-flow-notice": "", children: textOf('definitionFlow.loading', 'loading') })) : null, notices.map(note => (_jsx("div", { className: css.notice, "data-definition-flow-notice": "", children: note }, note)))] }));
|
|
107
135
|
}
|
|
@@ -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;
|
|
@@ -289,8 +295,9 @@ export interface SandoreFileOpenTarget {
|
|
|
289
295
|
*/
|
|
290
296
|
openArtifact(input: OpenArtifactInput): Promise<OpenOutcome>;
|
|
291
297
|
/**
|
|
292
|
-
* 打开一段 **Agent 输出文本**(result
|
|
293
|
-
*
|
|
298
|
+
* 打开一段 **Agent 输出文本**(result 叶子):**非空输出一律先落盘**(2026-09-22 真机报障后
|
|
299
|
+
* 去掉了"先确认能交出去"那道门),再走四段判定链;落盘的存在理由就是
|
|
300
|
+
* "`openTab`/编辑器只能打开磁盘上的文件"(见本函数内的完整结论)。
|
|
294
301
|
* @returns 四段判定链的 outcome(**永不 undefined**)。
|
|
295
302
|
*/
|
|
296
303
|
openOutput(input: OpenOutputInput): Promise<OpenOutcome>;
|
|
@@ -453,6 +460,24 @@ export declare function resolveSidebarPath(cwd: string | undefined, path: string
|
|
|
453
460
|
export declare function isWithinWorkspace(cwd: string | undefined, path: string): boolean;
|
|
454
461
|
/** 取路径末段(文件标题兜底;两侧分隔符都认)。 */
|
|
455
462
|
export declare function baseNameOf(path: string): string;
|
|
463
|
+
/**
|
|
464
|
+
* **纯函数**:这个绝对路径是不是**我们自己的 Agent 输出落点**
|
|
465
|
+
* (`<DSH_HOME>/storages/sandore/output/<instanceId>/<stepId>-<attemptIndex>-output.md`)。
|
|
466
|
+
*
|
|
467
|
+
* 为什么要一个"机器可判"的标记(2026-09-22 真机报障的根因之一):这个落点由 host 唯一构造
|
|
468
|
+
* (`instance-output.ts::outputDirPath`),**恒在会话 workspace 之外**;而 better-sidebar 的
|
|
469
|
+
* editor tab 读文件走它自己的 `/sidebar/api/fs.read`,**带 workspace fence**
|
|
470
|
+
* (0.18.0 `workspaceFence` 默认开 → `assertWithinWorkspace`,fence 根 = 落点会话 header 的 cwd)
|
|
471
|
+
* ⇒ 把 Agent 输出交给它的编辑器,只会换来它自己的 403/空白页 —— 我们这边又按"已交出去"清掉了
|
|
472
|
+
* 应用内预览 ⇒ **用户看到"点了没反应"**。
|
|
473
|
+
*
|
|
474
|
+
* `chain` 的 P14 例外(`ownerIsLanding` ⇒ 不判越界)是为**产物文件**写的(那种路径按会话 cwd
|
|
475
|
+
* 拼出来 ⇒ 必然在里面),**不能套到 Agent 输出上**:后者是"我们自己数据目录里的文件",
|
|
476
|
+
* 与会话 workspace 无关。有了这个标记就**不必依赖**客户端那份可能缺失/落后的 cwd 摘要。
|
|
477
|
+
* @param path - 目标绝对路径(相对路径一律 false)。
|
|
478
|
+
* @returns 是否落在 sandore 的 Agent 输出落点目录下。
|
|
479
|
+
*/
|
|
480
|
+
export declare function isSandoreOutputPath(path: string): boolean;
|
|
456
481
|
/** 取当前装配的打开面(未装配 ⇒ `undefined`,调用方仍必须给出可见结果)。 */
|
|
457
482
|
export declare function getSandoreFileOpen(): SandoreFileOpenTarget | undefined;
|
|
458
483
|
/**
|
|
@@ -157,6 +157,26 @@ export function baseNameOf(path) {
|
|
|
157
157
|
const index = unified.lastIndexOf('/');
|
|
158
158
|
return index === -1 ? unified : unified.slice(index + 1);
|
|
159
159
|
}
|
|
160
|
+
/**
|
|
161
|
+
* **纯函数**:这个绝对路径是不是**我们自己的 Agent 输出落点**
|
|
162
|
+
* (`<DSH_HOME>/storages/sandore/output/<instanceId>/<stepId>-<attemptIndex>-output.md`)。
|
|
163
|
+
*
|
|
164
|
+
* 为什么要一个"机器可判"的标记(2026-09-22 真机报障的根因之一):这个落点由 host 唯一构造
|
|
165
|
+
* (`instance-output.ts::outputDirPath`),**恒在会话 workspace 之外**;而 better-sidebar 的
|
|
166
|
+
* editor tab 读文件走它自己的 `/sidebar/api/fs.read`,**带 workspace fence**
|
|
167
|
+
* (0.18.0 `workspaceFence` 默认开 → `assertWithinWorkspace`,fence 根 = 落点会话 header 的 cwd)
|
|
168
|
+
* ⇒ 把 Agent 输出交给它的编辑器,只会换来它自己的 403/空白页 —— 我们这边又按"已交出去"清掉了
|
|
169
|
+
* 应用内预览 ⇒ **用户看到"点了没反应"**。
|
|
170
|
+
*
|
|
171
|
+
* `chain` 的 P14 例外(`ownerIsLanding` ⇒ 不判越界)是为**产物文件**写的(那种路径按会话 cwd
|
|
172
|
+
* 拼出来 ⇒ 必然在里面),**不能套到 Agent 输出上**:后者是"我们自己数据目录里的文件",
|
|
173
|
+
* 与会话 workspace 无关。有了这个标记就**不必依赖**客户端那份可能缺失/落后的 cwd 摘要。
|
|
174
|
+
* @param path - 目标绝对路径(相对路径一律 false)。
|
|
175
|
+
* @returns 是否落在 sandore 的 Agent 输出落点目录下。
|
|
176
|
+
*/
|
|
177
|
+
export function isSandoreOutputPath(path) {
|
|
178
|
+
return normalizeForCompare(path).includes('/storages/sandore/output/');
|
|
179
|
+
}
|
|
160
180
|
/** 模块级单例(组件树不携带 ctx;与 `installStepChatSessions` 同法)。 */
|
|
161
181
|
let installed;
|
|
162
182
|
/** 自注册 tab 的通用打开面(与 `installed` **同一处装配/卸载**:两者共享一个闭包)。 */
|
|
@@ -490,9 +510,20 @@ export function installSandoreFileOpen(deps) {
|
|
|
490
510
|
* 编辑器只会渲染它自己的 403 空壳 ⇒ 仍然跳过 L1/L2 走 L3(P13 的跨 workspace 正解不变)。
|
|
491
511
|
*/
|
|
492
512
|
const ownerIsLanding = request.instanceId !== '' && request.instanceId === landing.sessionId;
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
513
|
+
/*
|
|
514
|
+
* Agent 输出落点:**机器可判地在 workspace 之外** ⇒ 直接判越界(跳过 L1/L2,交给 L3)。
|
|
515
|
+
*
|
|
516
|
+
* 为什么必须单独判(2026-09-22 真机报障):下面那条 P14 例外把"产物属于落点会话"当成
|
|
517
|
+
* "路径必然在该会话 workspace 内"——那对**产物文件**成立(相对会话 cwd 拼出来),
|
|
518
|
+
* 但 Agent 输出自从搬到 `<DSH_HOME>/storages/sandore/output/` 之后**恒在外面**,
|
|
519
|
+
* 而且 `instanceId === landing.sessionId` 恰恰是**最常见**的情形(你就在看这个实例的会话)。
|
|
520
|
+
* ⇒ 落进 L1/L2:better-sidebar 的编辑器受自己的 workspace fence(默认开)拒绝,渲染 403/空白,
|
|
521
|
+
* 我们这边又以为"已经交出去了"而**不渲染应用内视图** ⇒ 用户看到"点了没反应"。
|
|
522
|
+
*/
|
|
523
|
+
const outside = isSandoreOutputPath(request.absolute)
|
|
524
|
+
|| (!ownerIsLanding
|
|
525
|
+
&& landing.cwd !== undefined
|
|
526
|
+
&& !isWithinWorkspace(landing.cwd, request.absolute));
|
|
496
527
|
if (outside) {
|
|
497
528
|
attempt({
|
|
498
529
|
kind: 'unusable',
|
|
@@ -556,7 +587,7 @@ export function installSandoreFileOpen(deps) {
|
|
|
556
587
|
return outcomeOf(read.reason, content);
|
|
557
588
|
};
|
|
558
589
|
/**
|
|
559
|
-
* **是否可以"交出去"
|
|
590
|
+
* **是否可以"交出去"**(同步判据;`sidebarAvailable()` 用;**不再参与落盘门**——见 `openOutput`)。
|
|
560
591
|
*
|
|
561
592
|
* 它回答的是"有没有落点":有落点会话 ∧(漏斗成员在场 ∨ editor tab 可用 ∨ 自注册 tab 可用)。
|
|
562
593
|
* 注意这是**必要条件**而不是承诺:真正交出去还要过 L1 的信封/探针与 L3 的注册表,所以
|
|
@@ -677,21 +708,22 @@ export function installSandoreFileOpen(deps) {
|
|
|
677
708
|
return outcomeOf('empty-text', content);
|
|
678
709
|
}
|
|
679
710
|
/*
|
|
680
|
-
* ──
|
|
711
|
+
* ── 「落盘」这条纪律(**2026-09-22 真机报障后改口径**)────────────────────────────
|
|
681
712
|
* 存在理由:**`openTab` / 编辑器 tab 只能打开磁盘上的文件** —— `seed.path` 就是 tab 的
|
|
682
713
|
* 内容种子(0.18.0 `SidebarTab.path`;0.19.x 的资源地址也由 path 编成)。Agent 输出只在
|
|
683
714
|
* 内存里,不落盘就没有 path ⇒ 只能自家渲染 ⇒ 正是用户不接受的那件事。
|
|
684
|
-
*
|
|
685
|
-
*
|
|
686
|
-
*
|
|
687
|
-
*
|
|
688
|
-
*
|
|
715
|
+
*
|
|
716
|
+
* **旧口径(已废)**:只有 `canHandOff()` 为真(有落点会话 ∧ 漏斗/编辑器/自注册 tab 至少一条)
|
|
717
|
+
* 才落盘。真机后果:**用户点「Agent 输出」后 `<DSH_HOME>/storages/sandore/output/` 压根没被创建**
|
|
718
|
+
* —— 用户据此直接问"是不是没把这个文件放到 output 目录"。而且落盘还有独立价值:
|
|
719
|
+
* ① 那是**用户的证据**(他自己也能去目录里找);② tab 的 `meta.path` 随布局持久化,
|
|
720
|
+
* 刷新后靠它重建内容;③ 与应用内兜底视图共用同一个原因行里的路径。
|
|
721
|
+
*
|
|
722
|
+
* **新口径**:**非空**输出 ⇒ **一律落盘**(零 gate),然后再走四段判定链;链上全不成 ⇒
|
|
723
|
+
* 应用内展示(内容来自内存文本,路径来自刚落盘的那个文件)。**空文本仍然不落盘**(决策 D4
|
|
724
|
+
* 不变:没有输出就没有文件,给中性提示)。落点固定在我们自己的数据目录、文件名由 step/attempt
|
|
725
|
+
* 派生 ⇒ 重复点击是覆盖,不是堆积。
|
|
689
726
|
*/
|
|
690
|
-
if (!canHandOff()) {
|
|
691
|
-
const failure = landingFailureOf();
|
|
692
|
-
const content = artifactContentOf({ kind: 'result', name, text: input.text, reason: failure.reason, message: failure.message });
|
|
693
|
-
return outcomeOf(failure.reason, content);
|
|
694
|
-
}
|
|
695
727
|
let written;
|
|
696
728
|
try {
|
|
697
729
|
const writeOutput = deps.writeOutput;
|
|
@@ -40,7 +40,14 @@ export type CommandType = CommandConfig['type'];
|
|
|
40
40
|
export declare const COMMAND_TYPES: readonly CommandType[];
|
|
41
41
|
/** 生成带前缀的唯一 id(`{prefix}-{uuid}`),供 Stage/Step/ipt/opt 节点复用。 */
|
|
42
42
|
export declare function newId(prefix: string): string;
|
|
43
|
-
/**
|
|
43
|
+
/**
|
|
44
|
+
* 构造一个新的空 microflow 定义(唯一 `mfl-{uuid}` id + 空 steps)。
|
|
45
|
+
*
|
|
46
|
+
* ★ **前缀 = `mfl-`**(用户口径,2026-09-21 定稿):`microflow` 重构(D4b)之后对外**零别名**都叫
|
|
47
|
+
* microflow,id 前缀却还留着旧词 `stg-`(stage)——那是历史遗留,现已改成 `mfl-`。
|
|
48
|
+
* **只管"新铸的 id"**:既有数据里的 `stg-…` id 是**不透明字符串**(没有任何代码解析前缀),
|
|
49
|
+
* 照旧合法、**不要**去批量改写它们(改 id 等于改介质契约)。
|
|
50
|
+
*/
|
|
44
51
|
export declare function createMicroflow(name: string): MicroflowDefinition;
|
|
45
52
|
/** 构造一个新的空 microflow 定义(旧名保留为别名,语义相同)。 */
|
|
46
53
|
export declare const createStage: typeof createMicroflow;
|
|
@@ -37,9 +37,16 @@ export const COMMAND_TYPES = ['shell', 'prompt', 'agent', 'skill', 'mcp'];
|
|
|
37
37
|
export function newId(prefix) {
|
|
38
38
|
return `${prefix}-${globalThis.crypto.randomUUID()}`;
|
|
39
39
|
}
|
|
40
|
-
/**
|
|
40
|
+
/**
|
|
41
|
+
* 构造一个新的空 microflow 定义(唯一 `mfl-{uuid}` id + 空 steps)。
|
|
42
|
+
*
|
|
43
|
+
* ★ **前缀 = `mfl-`**(用户口径,2026-09-21 定稿):`microflow` 重构(D4b)之后对外**零别名**都叫
|
|
44
|
+
* microflow,id 前缀却还留着旧词 `stg-`(stage)——那是历史遗留,现已改成 `mfl-`。
|
|
45
|
+
* **只管"新铸的 id"**:既有数据里的 `stg-…` id 是**不透明字符串**(没有任何代码解析前缀),
|
|
46
|
+
* 照旧合法、**不要**去批量改写它们(改 id 等于改介质契约)。
|
|
47
|
+
*/
|
|
41
48
|
export function createMicroflow(name) {
|
|
42
|
-
return { id: newId('
|
|
49
|
+
return { id: newId('mfl'), name, steps: [] };
|
|
43
50
|
}
|
|
44
51
|
/** 构造一个新的空 microflow 定义(旧名保留为别名,语义相同)。 */
|
|
45
52
|
export const createStage = createMicroflow;
|
|
@@ -175,7 +182,7 @@ export const NEW_DEFINITION_ID = 'wfl-new';
|
|
|
175
182
|
* @returns 最小合法定义。
|
|
176
183
|
*/
|
|
177
184
|
export function createBlankDefinition(id = NEW_DEFINITION_ID) {
|
|
178
|
-
const microflowId = newId('
|
|
185
|
+
const microflowId = newId('mfl');
|
|
179
186
|
return {
|
|
180
187
|
workflow: {
|
|
181
188
|
id,
|
|
@@ -27,6 +27,21 @@
|
|
|
27
27
|
* ## 幂等
|
|
28
28
|
* 打开动作按 **`callId`** 记在模块级 `Set`(同一工具行重渲/重挂不再开;`dedupeKey` 兜住
|
|
29
29
|
* "同一定义只留一个 tab")。两条都由本模块的 `definitionFlowTabOpenAlready` 与 spec 锁住。
|
|
30
|
+
*
|
|
31
|
+
* ## 「打开意图」版本号(2026-09-22 真机报障:**右边那张图只渲染一次**)
|
|
32
|
+
* 现象:模型连着调了好几次 `visualize`(回执 `stepCount` 从 0 一路变到 17),侧栏那张图**始终是
|
|
33
|
+
* 第一次**的样子。
|
|
34
|
+
*
|
|
35
|
+
* 根因(**不在我们这侧**):better-sidebar 0.18.0 的 `applyDedupe`(`service.ts:842-865`)在
|
|
36
|
+
* `dedupeKey` 命中已有 tab 时**只 `activateTabReducer` 聚焦**——新 `seed` 整条被丢弃、tab 体
|
|
37
|
+
* **不重挂**、`meta` 也不刷新。而本 tab 体唯一的"重取开关"是它自己的 `revision` 计数器
|
|
38
|
+
* (初值 0,只被步骤弹窗保存 +1)⇒ 第二次起 `useEffect` 的依赖逐值不变 ⇒ **永不重取**。
|
|
39
|
+
* 侧栏在场时行内图**又不渲染**(`definition-flow-tool-card.tsx` 的 `handedOff`)⇒ 一个可见面都不更新。
|
|
40
|
+
*
|
|
41
|
+
* 处置:本模块记一个**按 `workflowId` 分桶的「打开意图」计数**,**每次成功把图交给侧栏**
|
|
42
|
+
* (`result.kind === 'opened'`,**含"聚焦已有 tab"那一路**)就 +1 并通知订阅者;tab 体用
|
|
43
|
+
* `useSyncExternalStore` 读它、与本地保存计数相加当取数开关(见 `definition-flow-tab.tsx`)。
|
|
44
|
+
* 这样"模型又画了一次"就一定会让那张图重取,而**不需要**动 host / wire / typert / SSE 契约。
|
|
30
45
|
* @module @deepseek-ai/dsh-client-ui-sandore/src/client/definition-flow-open
|
|
31
46
|
*/
|
|
32
47
|
import { type SandoreTabOpenReason } from './better-sidebar-open.ts';
|
|
@@ -79,13 +94,36 @@ export interface DefinitionFlowOpenInput {
|
|
|
79
94
|
*/
|
|
80
95
|
readonly callId?: string | undefined;
|
|
81
96
|
}
|
|
97
|
+
/**
|
|
98
|
+
* 读某份定义的「打开意图」计数(没有记录 ⇒ 0)。
|
|
99
|
+
* @param workflowId - 定义 id。
|
|
100
|
+
* @returns 该定义被"打开/聚焦"过多少次(从进程启动起算)。
|
|
101
|
+
*/
|
|
102
|
+
export declare function definitionFlowRevisionOf(workflowId: string): number;
|
|
103
|
+
/**
|
|
104
|
+
* 订阅「打开意图」计数变化。
|
|
105
|
+
* @param listener - 变化回调(`useSyncExternalStore` 的 subscribe 参数)。
|
|
106
|
+
* @returns 取消订阅。
|
|
107
|
+
*/
|
|
108
|
+
export declare function subscribeDefinitionFlowRevision(listener: () => void): () => void;
|
|
109
|
+
/**
|
|
110
|
+
* 给某份定义的「打开意图」计数 +1 并通知订阅者(**永不抛**:单个订阅者出错不影响其它人)。
|
|
111
|
+
* @param workflowId - 定义 id。
|
|
112
|
+
* @returns 新的计数。
|
|
113
|
+
*/
|
|
114
|
+
export declare function bumpDefinitionFlowRevision(workflowId: string): number;
|
|
82
115
|
/**
|
|
83
116
|
* 这次工具调用**已经**把 tab 开过了吗(幂等门)。
|
|
84
117
|
* @param callId - 工具调用 id。
|
|
85
118
|
* @returns 是否已开过。
|
|
86
119
|
*/
|
|
87
120
|
export declare function definitionFlowTabOpenAlready(callId: string): boolean;
|
|
88
|
-
/**
|
|
121
|
+
/**
|
|
122
|
+
* 清空幂等门与「打开意图」计数(**只给单测**:模块级状态不能跨用例泄漏)。
|
|
123
|
+
*
|
|
124
|
+
* 通知订阅者但**不回退**计数语义:tab 体只对**递增**做出反应(见 `definition-flow-tab.tsx`),
|
|
125
|
+
* 所以清空对已挂载的组件是**不可见**的,不会凭空触发一次取数。
|
|
126
|
+
*/
|
|
89
127
|
export declare function resetDefinitionFlowOpens(): void;
|
|
90
128
|
/**
|
|
91
129
|
* 打开「定义流程图」tab(**同步、先算后调、永不抛**)。
|
|
@@ -27,6 +27,21 @@
|
|
|
27
27
|
* ## 幂等
|
|
28
28
|
* 打开动作按 **`callId`** 记在模块级 `Set`(同一工具行重渲/重挂不再开;`dedupeKey` 兜住
|
|
29
29
|
* "同一定义只留一个 tab")。两条都由本模块的 `definitionFlowTabOpenAlready` 与 spec 锁住。
|
|
30
|
+
*
|
|
31
|
+
* ## 「打开意图」版本号(2026-09-22 真机报障:**右边那张图只渲染一次**)
|
|
32
|
+
* 现象:模型连着调了好几次 `visualize`(回执 `stepCount` 从 0 一路变到 17),侧栏那张图**始终是
|
|
33
|
+
* 第一次**的样子。
|
|
34
|
+
*
|
|
35
|
+
* 根因(**不在我们这侧**):better-sidebar 0.18.0 的 `applyDedupe`(`service.ts:842-865`)在
|
|
36
|
+
* `dedupeKey` 命中已有 tab 时**只 `activateTabReducer` 聚焦**——新 `seed` 整条被丢弃、tab 体
|
|
37
|
+
* **不重挂**、`meta` 也不刷新。而本 tab 体唯一的"重取开关"是它自己的 `revision` 计数器
|
|
38
|
+
* (初值 0,只被步骤弹窗保存 +1)⇒ 第二次起 `useEffect` 的依赖逐值不变 ⇒ **永不重取**。
|
|
39
|
+
* 侧栏在场时行内图**又不渲染**(`definition-flow-tool-card.tsx` 的 `handedOff`)⇒ 一个可见面都不更新。
|
|
40
|
+
*
|
|
41
|
+
* 处置:本模块记一个**按 `workflowId` 分桶的「打开意图」计数**,**每次成功把图交给侧栏**
|
|
42
|
+
* (`result.kind === 'opened'`,**含"聚焦已有 tab"那一路**)就 +1 并通知订阅者;tab 体用
|
|
43
|
+
* `useSyncExternalStore` 读它、与本地保存计数相加当取数开关(见 `definition-flow-tab.tsx`)。
|
|
44
|
+
* 这样"模型又画了一次"就一定会让那张图重取,而**不需要**动 host / wire / typert / SSE 契约。
|
|
30
45
|
* @module @deepseek-ai/dsh-client-ui-sandore/src/client/definition-flow-open
|
|
31
46
|
*/
|
|
32
47
|
import { getSandoreTabOpener } from "./better-sidebar-open.js";
|
|
@@ -53,6 +68,52 @@ export function definitionFlowTabInstanceId(workflowId) {
|
|
|
53
68
|
}
|
|
54
69
|
/** 已成功打开过的 `callId`(模块级;`resetDefinitionFlowOpens` 只给单测用)。 */
|
|
55
70
|
const openedCallIds = new Set();
|
|
71
|
+
/**
|
|
72
|
+
* 「打开意图」计数(按 `workflowId` 分桶)。
|
|
73
|
+
*
|
|
74
|
+
* 每成功把图交给侧栏(含"聚焦已有 tab"那一路)就 +1 —— 它是 tab 体重取的**唯一**外部信号,
|
|
75
|
+
* 因为 better-sidebar 在 `dedupeKey` 命中时不换 seed、不重挂、不刷新 `meta`(模块头有根因)。
|
|
76
|
+
*/
|
|
77
|
+
const openRevisions = new Map();
|
|
78
|
+
/** 打开意图计数的订阅者(tab 体经 `useSyncExternalStore` 订阅)。 */
|
|
79
|
+
const openRevisionListeners = new Set();
|
|
80
|
+
/**
|
|
81
|
+
* 读某份定义的「打开意图」计数(没有记录 ⇒ 0)。
|
|
82
|
+
* @param workflowId - 定义 id。
|
|
83
|
+
* @returns 该定义被"打开/聚焦"过多少次(从进程启动起算)。
|
|
84
|
+
*/
|
|
85
|
+
export function definitionFlowRevisionOf(workflowId) {
|
|
86
|
+
return openRevisions.get(workflowId) ?? 0;
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* 订阅「打开意图」计数变化。
|
|
90
|
+
* @param listener - 变化回调(`useSyncExternalStore` 的 subscribe 参数)。
|
|
91
|
+
* @returns 取消订阅。
|
|
92
|
+
*/
|
|
93
|
+
export function subscribeDefinitionFlowRevision(listener) {
|
|
94
|
+
openRevisionListeners.add(listener);
|
|
95
|
+
return () => {
|
|
96
|
+
openRevisionListeners.delete(listener);
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
/**
|
|
100
|
+
* 给某份定义的「打开意图」计数 +1 并通知订阅者(**永不抛**:单个订阅者出错不影响其它人)。
|
|
101
|
+
* @param workflowId - 定义 id。
|
|
102
|
+
* @returns 新的计数。
|
|
103
|
+
*/
|
|
104
|
+
export function bumpDefinitionFlowRevision(workflowId) {
|
|
105
|
+
const next = definitionFlowRevisionOf(workflowId) + 1;
|
|
106
|
+
openRevisions.set(workflowId, next);
|
|
107
|
+
for (const listener of [...openRevisionListeners]) {
|
|
108
|
+
try {
|
|
109
|
+
listener();
|
|
110
|
+
}
|
|
111
|
+
catch (error) {
|
|
112
|
+
console.warn('[sandore] a definition-flow revision listener threw:', error);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
return next;
|
|
116
|
+
}
|
|
56
117
|
/**
|
|
57
118
|
* 这次工具调用**已经**把 tab 开过了吗(幂等门)。
|
|
58
119
|
* @param callId - 工具调用 id。
|
|
@@ -61,9 +122,23 @@ const openedCallIds = new Set();
|
|
|
61
122
|
export function definitionFlowTabOpenAlready(callId) {
|
|
62
123
|
return callId !== '' && openedCallIds.has(callId);
|
|
63
124
|
}
|
|
64
|
-
/**
|
|
125
|
+
/**
|
|
126
|
+
* 清空幂等门与「打开意图」计数(**只给单测**:模块级状态不能跨用例泄漏)。
|
|
127
|
+
*
|
|
128
|
+
* 通知订阅者但**不回退**计数语义:tab 体只对**递增**做出反应(见 `definition-flow-tab.tsx`),
|
|
129
|
+
* 所以清空对已挂载的组件是**不可见**的,不会凭空触发一次取数。
|
|
130
|
+
*/
|
|
65
131
|
export function resetDefinitionFlowOpens() {
|
|
66
132
|
openedCallIds.clear();
|
|
133
|
+
openRevisions.clear();
|
|
134
|
+
for (const listener of [...openRevisionListeners]) {
|
|
135
|
+
try {
|
|
136
|
+
listener();
|
|
137
|
+
}
|
|
138
|
+
catch {
|
|
139
|
+
/* 单测清理阶段:订阅者抛错无意义,吞掉 */
|
|
140
|
+
}
|
|
141
|
+
}
|
|
67
142
|
}
|
|
68
143
|
/**
|
|
69
144
|
* 打开「定义流程图」tab(**同步、先算后调、永不抛**)。
|
|
@@ -103,6 +178,9 @@ export function openDefinitionFlowTab(input) {
|
|
|
103
178
|
if (result.kind === 'opened') {
|
|
104
179
|
if (callId !== '')
|
|
105
180
|
openedCallIds.add(callId);
|
|
181
|
+
// 「打开意图」+1:**含**"dedupeKey 命中 ⇒ 只聚焦了已有 tab"那一路(那条路上
|
|
182
|
+
// better-sidebar 不换 seed、不重挂 tab 体 ⇒ 只有这个计数能让那张图重取)。
|
|
183
|
+
bumpDefinitionFlowRevision(workflowId);
|
|
106
184
|
return { kind: 'sidebar-open' };
|
|
107
185
|
}
|
|
108
186
|
return { kind: 'inline', reason: result.reason, message: result.message };
|