dsh-data-cleaning-agent 0.9.7 → 0.9.8
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/CHANGELOG.md +7 -0
- package/README.en.md +3 -1
- package/README.md +4 -2
- package/docs/RELEASE-0.9.8.md +68 -0
- package/lib/client.js +92 -24
- package/lib/image-workflow.js +1 -0
- package/lib/qcc-safety.js +1 -0
- package/lib/qcc.js +18 -2
- package/lib/web.js +4 -0
- package/lib/workflow-contract.js +13 -1
- package/lib/workflow-execution.js +1 -1
- package/lib/workflow.js +13 -0
- package/package.json +2 -1
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,13 @@
|
|
|
4
4
|
|
|
5
5
|
## [Unreleased]
|
|
6
6
|
|
|
7
|
+
## [0.9.8] - 2026-09-13
|
|
8
|
+
|
|
9
|
+
- DC-SCOPE-P0:持久保存任务来源 Session/Workspace 与安全展示名称;v2 旧记录采用附加字段兼容读取,不猜测来源、不批量改写历史。
|
|
10
|
+
- Profile 历史改为独立只读详情;打开来源会话按宿主能力降级,不覆盖当前任务、规则、名单或命令。保留客户 XLSX 与次要任务报告分层。
|
|
11
|
+
- 当前绑定恢复、迟到刷新及 HTTP 写入/命令准备校验来源;跨会话请求拒绝,版本冲突仍由 Host revision 控制。
|
|
12
|
+
- 审计增加 Provider 六类状态区分;完成态迟到更新、A/B、旧记录、重启和来源导航补充回归。真实 DSH Web 未认证,真实宿主与 Provider 未验证。
|
|
13
|
+
|
|
7
14
|
## [0.9.7] - 2026-09-11
|
|
8
15
|
|
|
9
16
|
- 优化数据清洗补全工作台的区域间距、信息层级与明暗主题响应式布局,重构导入清单、字段映射、质量体检和主体匹配页面。
|
package/README.en.md
CHANGED
|
@@ -10,7 +10,9 @@ Related agents: [数据清洗补全](https://github.com/duhu2000/dsh-data-cleani
|
|
|
10
10
|
|
|
11
11
|
> A data cleaning & completion agent plugin for DeepSeek Harness: local CSV/XLSX/JSON engine plus optional Qichacha (QCC) MCP enterprise-data enrichment. Initiated and maintained by the Qichacha (QCC) team.
|
|
12
12
|
>
|
|
13
|
-
> Current source version / 当前源码版本: **0.9.
|
|
13
|
+
> Current source version / 当前源码版本: **0.9.8** (stable release)
|
|
14
|
+
|
|
15
|
+
0.9.8 makes Profile history read-only and persists origin Workspace/Session metadata. Legacy records remain explicitly unattributed and cannot become the current Session task. Open-origin navigation is capability-gated; existing XLSX artifacts remain downloadable without MCP calls. Rollback: `dsh plugin --profile web add dsh-data-cleaning-agent@0.9.7` (reintroduces the old history-binding risk). Live DSH A/B verification is unverified because the local Web session requires authentication; see the release checklist.
|
|
14
16
|
|
|
15
17
|
This release refines workbench hierarchy, spacing, and responsive behavior across imported lists, field mapping, quality profiling, and entity matching. File, image, and pasted-data inputs collapse after a successful import and return through the explicit reimport action. Field mapping uses grouped choices, and searching a dimension exposes all fields in that group. Preparing a draft does not query QCC; sending starts execution. Fully restart DSH after upgrading. Existing download artifacts are not rewritten.
|
|
16
18
|
|
package/README.md
CHANGED
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
数据清洗补全智能体:面向 Excel/CSV/JSON 企业名单,提供数据清洗、表格清洗、清洗补全、去重、企业数据补全与字段补全,支持企查查 MCP 和结果导出。
|
|
10
10
|
|
|
11
11
|
```sh
|
|
12
|
-
dsh plugin --profile web add dsh-data-cleaning-agent@0.9.
|
|
12
|
+
dsh plugin --profile web add dsh-data-cleaning-agent@0.9.8
|
|
13
13
|
```
|
|
14
14
|
|
|
15
15
|
请先满足下文的 DSH、连接器及侧边栏依赖要求;安装后完整停止并重启对应 Profile。
|
|
@@ -27,7 +27,9 @@ dsh plugin --profile web add dsh-data-cleaning-agent@0.9.7
|
|
|
27
27
|
|
|
28
28
|
> 在 DeepSeek Harness 中清洗、补全、画像企业名单数据的智能体插件:本地 CSV/XLSX/JSON 引擎 + 可选企查查(Qichacha/QCC)MCP 企业数据补全,由企查查(Qichacha/QCC)团队发起并维护。
|
|
29
29
|
>
|
|
30
|
-
> 当前源码版本 / Current source version: **0.9.
|
|
30
|
+
> 当前源码版本 / Current source version: **0.9.8**(正式版本)
|
|
31
|
+
|
|
32
|
+
0.9.8 修复历史任务跨 Session 误绑定:历史默认只读,显示来源工作区/会话;旧记录显示“历史版本未保存来源”,不自动接管。来源可用时可“打开来源会话”,不会修改当前任务。已有 XLSX 与任务报告可继续下载,导航不调用 MCP。升级后旧版本当前绑定将进入只读历史;新任务按 Session 恢复。回滚:`dsh plugin --profile web add dsh-data-cleaning-agent@0.9.7`(回滚会重新暴露旧版误绑定风险)。真实 DSH A/B 验收因本机 Web 未认证而未验证,详见发布清单。
|
|
31
33
|
|
|
32
34
|
本版优化工作台的信息层级、区域间距与响应式布局,重构导入清单、字段映射、质量体检及主体匹配页面。导入成功后文件、图片与粘贴入口自动收起,通过“重新导入”再次展开;映射字段按维度分组选择,搜索维度名称时展示该组全部字段。生成说明不查询,发送才执行;升级后需完整重启 DSH,历史下载文件不自动改写。
|
|
33
35
|
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
# 0.9.8 · DC-SCOPE-P0
|
|
2
|
+
|
|
3
|
+
Status: release candidate. Date: 2026-09-13.
|
|
4
|
+
|
|
5
|
+
## Scope and authorization
|
|
6
|
+
|
|
7
|
+
Explicit ledger delegation authorizes code, tests, commit/push, PR merge, immutable tag,
|
|
8
|
+
GitHub Release, OIDC npm publishing and registry read-back. Base main: 2424c97.
|
|
9
|
+
Package: dsh-data-cleaning-agent 0.9.8; qcc-field-contracts stays 0.2.0.
|
|
10
|
+
Adopts DSH-UX-001 v1.5.3 at the shared AI-设计/DSH智能体开发交互规范方案.md;
|
|
11
|
+
visual reference: DSH智能体_企查查蓝_UI_Mockup_v1.5.0.html.
|
|
12
|
+
|
|
13
|
+
## Migration and boundaries
|
|
14
|
+
|
|
15
|
+
- New UI tasks persist immutable originSessionId/originWorkspaceId and bounded display labels.
|
|
16
|
+
Missing Workspace capability is explicitly unknown; never inferred from active/recent Workspace.
|
|
17
|
+
- Schema v2 additive read migration returns null origin for legacy records. No bulk rewrite.
|
|
18
|
+
- LocalStorage bindings are hints, not ownership proof. Wrong/legacy bindings cannot hydrate
|
|
19
|
+
the current Session; explicit new work creates a new task. Profile history remains readable.
|
|
20
|
+
- History selection has separate state and uses GET only. It does not load run rows, rules,
|
|
21
|
+
field selection, paid confirmations or command credentials into the current Session.
|
|
22
|
+
- Open-source-Session uses an existing, capability-verified Session only. Missing/deleted
|
|
23
|
+
origins remain read-only; no clone or silent rebind feature is introduced.
|
|
24
|
+
- HTTP task mutations and workflow command preparation require matching persisted origin
|
|
25
|
+
for scoped tasks. This is an application isolation guard, not an authentication boundary:
|
|
26
|
+
existing Host same-origin/Profile access controls remain authoritative. Legacy/headless
|
|
27
|
+
origin-less API records remain backwards-compatible; UI never adopts them as current.
|
|
28
|
+
- Revision conflict rejects concurrent updates; stale UI snapshots do not overwrite newer ones.
|
|
29
|
+
- Customer XLSX and secondary report links remain separated. Downloads reuse saved artifacts.
|
|
30
|
+
- Provider audit distinguishes success-data, success-empty, not-required, no-permission,
|
|
31
|
+
failed, unknown without changing field completeness or charging rules. No raw response saved.
|
|
32
|
+
- Completed/cancelled workflow states cannot regress. Existing explicit partial-result retry
|
|
33
|
+
is retained; it is not classified as an immutable successful completion.
|
|
34
|
+
|
|
35
|
+
## Verification
|
|
36
|
+
|
|
37
|
+
Local evidence (2026-09-13): `npm run check` passed, 275/275 tests, zero failures.
|
|
38
|
+
Real React/Chromium layout matrix passed 10/10 (light/dark; 320–1440px).
|
|
39
|
+
Artifact preview passed 390/800/1440px with compact rows, two-axis scrolling and fixed headers.
|
|
40
|
+
`git diff --check` passed. Remote CI and registry evidence are pending publication.
|
|
41
|
+
No separate compiled build: runtime consists of shipped JS;
|
|
42
|
+
`npm run check` includes syntax, documentation, package allowlist and full node tests.
|
|
43
|
+
Real React/Chromium layout and preview regressions are separate from live DSH.
|
|
44
|
+
Tests cover Session A/B, origin persistence across store restart, legacy unknown records,
|
|
45
|
+
concurrent revision updates, stale history/current responses, open-origin capability checks,
|
|
46
|
+
Tab X/fold/provider-remount and synthetic four-plugin coexistence baseline from PR #13.
|
|
47
|
+
|
|
48
|
+
**真实 DSH Session A/B 浏览器验收:因本机 DSH Web 未认证而未验证。**
|
|
49
|
+
Observed browser text: “dsh web authentication required; reopen the URL printed by dsh web.”
|
|
50
|
+
No authentication bypass or token collection attempted. Ledger confirmed this external
|
|
51
|
+
condition does not block release if all automated gates pass. Live QCC/OCR/Windows-host
|
|
52
|
+
and real four-plugin installation verification are not claimed. Market publishing out of scope.
|
|
53
|
+
|
|
54
|
+
Minimal user recheck after authentication/restart:
|
|
55
|
+
1. In Session A, create a synthetic task without sending paid commands; confirm origin labels.
|
|
56
|
+
2. In Session B, enter a different draft/field selection, view A from history, then return to
|
|
57
|
+
current task. B's draft, fields, taskId and stage must remain unchanged.
|
|
58
|
+
3. Download A's saved artifact without new MCP calls; open source Session A only via explicit action.
|
|
59
|
+
4. Restart/reload, close Tab X, fold/unfold Sidebar, switch among four plugins and repeat.
|
|
60
|
+
Old tasks must say “历史版本未保存来源” and must never replace B's current task.
|
|
61
|
+
|
|
62
|
+
## Publish and rollback
|
|
63
|
+
|
|
64
|
+
Follow PR → exact merge main CI → annotated v0.9.8 → Release workflow OIDC/provenance.
|
|
65
|
+
Read back npm latest/version/gitHead/attestations, tag SHA and GitHub Release flags.
|
|
66
|
+
Rollback package: `dsh plugin --profile web add dsh-data-cleaning-agent@0.9.7`.
|
|
67
|
+
Rollback reintroduces history adoption risk; preserve persisted records and do not move tags.
|
|
68
|
+
Release evidence will be appended after publication; no market or user acceptance claim.
|
package/lib/client.js
CHANGED
|
@@ -1941,8 +1941,20 @@ body:has(.dcAgentPromptBackdrop) { overflow: hidden; }
|
|
|
1941
1941
|
const time = date && Number.isFinite(date.getTime()) ? date.toLocaleString() : '';
|
|
1942
1942
|
return `${base}|${task.source?.type === 'image' && task.state === 'draft' ? '待识别' : `${task.source?.rowCount ?? 0} 条`}${time ? '|' + time : ''}`;
|
|
1943
1943
|
}
|
|
1944
|
+
function taskBelongsToSession(task, sessionId) {
|
|
1945
|
+
if (!sessionId || task?.originSessionId !== sessionId) return false;
|
|
1946
|
+
const scope = sessionWorkbenchController?.originFor?.(sessionId);
|
|
1947
|
+
return !scope?.originWorkspaceId || task.originWorkspaceId === scope.originWorkspaceId;
|
|
1948
|
+
}
|
|
1949
|
+
function workflowOriginLabel(task) {
|
|
1950
|
+
if (!task?.originSessionId) return '历史版本未保存来源';
|
|
1951
|
+
return `工作区:${task.originWorkspaceName || task.originWorkspaceId || '未取得来源工作区'} · 会话:${task.originSessionName || task.originSessionId}`;
|
|
1952
|
+
}
|
|
1944
1953
|
function cacheWorkflowTask(actions, sessionId, task) {
|
|
1945
1954
|
if (!task) return null;
|
|
1955
|
+
if (!taskBelongsToSession(task, sessionId)) throw new Error('此任务不属于当前会话;请在任务历史中只读查看或打开来源会话。');
|
|
1956
|
+
const previous = workflowTaskBySession.get(String(sessionId));
|
|
1957
|
+
if (previous?.id === task.id && previous.revision > task.revision) return previous;
|
|
1946
1958
|
if (sessionId) workflowTaskBySession.set(String(sessionId), task);
|
|
1947
1959
|
bindTask(sessionId, task.id);
|
|
1948
1960
|
actions.setWorkflowTask(task);
|
|
@@ -1955,10 +1967,13 @@ body:has(.dcAgentPromptBackdrop) { overflow: hidden; }
|
|
|
1955
1967
|
}
|
|
1956
1968
|
|
|
1957
1969
|
async function createWorkflowTask(actions, sessionId, draft = {}) {
|
|
1970
|
+
if (!sessionId) throw new Error('请先打开数据清洗会话。');
|
|
1958
1971
|
const key = String(sessionId || 'unassigned');
|
|
1959
1972
|
let pending = workflowTaskCreationBySession.get(key);
|
|
1960
1973
|
if (!pending) {
|
|
1961
1974
|
pending = requestJson('/data-cleaning/api/workflow/tasks', 'POST', {
|
|
1975
|
+
...sessionWorkbenchController?.originFor?.(sessionId),
|
|
1976
|
+
originSessionId: sessionId,
|
|
1962
1977
|
title: draft.title || '企业数据清洗补全任务',
|
|
1963
1978
|
objectives: draft.objectives || DEFAULT_CLEANING_KEYS,
|
|
1964
1979
|
fieldSelection: draft.fieldSelection || DEFAULT_ENRICHMENT_KEYS,
|
|
@@ -1984,9 +1999,12 @@ body:has(.dcAgentPromptBackdrop) { overflow: hidden; }
|
|
|
1984
1999
|
const saved = readTaskBindings()[key];
|
|
1985
2000
|
if (ownedSessionId(key) && /^dcw-[a-zA-Z0-9-]+$/.test(saved || '')) {
|
|
1986
2001
|
const response = await requestJson(`/data-cleaning/api/workflow/tasks/${encodeURIComponent(saved)}`);
|
|
1987
|
-
|
|
1988
|
-
|
|
1989
|
-
|
|
2002
|
+
if (taskBelongsToSession(response.task, sessionId)) {
|
|
2003
|
+
const task = cacheWorkflowTask(actions, sessionId, response.task);
|
|
2004
|
+
if (restoreStage) actions.setStep(task.stage || 'upload');
|
|
2005
|
+
return task;
|
|
2006
|
+
}
|
|
2007
|
+
bindTask(sessionId, null);
|
|
1990
2008
|
}
|
|
1991
2009
|
return createWorkflowTask(actions, sessionId, draft);
|
|
1992
2010
|
}
|
|
@@ -2000,6 +2018,7 @@ body:has(.dcAgentPromptBackdrop) { overflow: hidden; }
|
|
|
2000
2018
|
async function ensureEditableWorkflowTask(actions, sessionId, draft = {}, preferredTask) {
|
|
2001
2019
|
const key = String(sessionId || 'unassigned');
|
|
2002
2020
|
const current = workflowTaskBySession.get(key) ?? preferredTask ?? null;
|
|
2021
|
+
if (current && !taskBelongsToSession(current, sessionId)) throw new Error('不能将历史任务绑定到当前会话。请新建清洗任务。');
|
|
2003
2022
|
if (!current) return createWorkflowTask(actions, sessionId, draft);
|
|
2004
2023
|
if (EDITABLE_WORKFLOW_STATES.has(current.state)) return cacheWorkflowTask(actions, sessionId, current);
|
|
2005
2024
|
workflowTaskBySession.delete(key);
|
|
@@ -2023,8 +2042,10 @@ body:has(.dcAgentPromptBackdrop) { overflow: hidden; }
|
|
|
2023
2042
|
async function updateWorkflowTask(actions, sessionId, task, input) {
|
|
2024
2043
|
const latest = workflowTaskBySession.get(String(sessionId || 'unassigned')) ?? task;
|
|
2025
2044
|
if (!latest) return ensureWorkflowTask(actions, sessionId, input);
|
|
2045
|
+
if (!taskBelongsToSession(latest, sessionId)) throw new Error('此任务不属于当前会话。');
|
|
2026
2046
|
const response = await requestJson(`/data-cleaning/api/workflow/tasks/${encodeURIComponent(latest.id)}`, 'PATCH', {
|
|
2027
2047
|
...input,
|
|
2048
|
+
originSessionId: sessionId, originWorkspaceId: latest.originWorkspaceId,
|
|
2028
2049
|
expectedRevision: latest.revision,
|
|
2029
2050
|
});
|
|
2030
2051
|
return cacheWorkflowTask(actions, sessionId, response.task);
|
|
@@ -2033,8 +2054,10 @@ body:has(.dcAgentPromptBackdrop) { overflow: hidden; }
|
|
|
2033
2054
|
async function workflowAction(actions, sessionId, task, action, input = {}) {
|
|
2034
2055
|
const latest = workflowTaskBySession.get(String(sessionId || 'unassigned')) ?? task;
|
|
2035
2056
|
if (!latest) throw new Error('请先创建数据清洗补全任务。');
|
|
2057
|
+
if (!taskBelongsToSession(latest, sessionId)) throw new Error('此任务不属于当前会话。');
|
|
2036
2058
|
const response = await requestJson(`/data-cleaning/api/workflow/tasks/${encodeURIComponent(latest.id)}/actions/${action}`, 'POST', {
|
|
2037
2059
|
...input,
|
|
2060
|
+
originSessionId: sessionId, originWorkspaceId: latest.originWorkspaceId,
|
|
2038
2061
|
expectedRevision: latest.revision,
|
|
2039
2062
|
});
|
|
2040
2063
|
return cacheWorkflowTask(actions, sessionId, response.task);
|
|
@@ -2048,6 +2071,7 @@ body:has(.dcAgentPromptBackdrop) { overflow: hidden; }
|
|
|
2048
2071
|
if (JSON.stringify([...(task.fieldSelection || [])].sort()) !== JSON.stringify([...(expected.enrichmentKeys || [])].sort())) throw new Error('图片任务字段配置尚未保存,请重试。');
|
|
2049
2072
|
const bound = await requestJson(`/data-cleaning/api/images/commands/${encodeURIComponent(expected.imageCommandId)}`, 'PATCH', {
|
|
2050
2073
|
taskId: task.id, expectedRevision: task.revision, executeOnSend: true,
|
|
2074
|
+
originSessionId: sessionId, originWorkspaceId: task.originWorkspaceId,
|
|
2051
2075
|
});
|
|
2052
2076
|
return { ...bound.command, prompt: buildTaskPrompt(expected) };
|
|
2053
2077
|
}
|
|
@@ -2071,6 +2095,7 @@ body:has(.dcAgentPromptBackdrop) { overflow: hidden; }
|
|
|
2071
2095
|
});
|
|
2072
2096
|
const response = await requestJson('/data-cleaning/api/g5/commands', 'POST', {
|
|
2073
2097
|
kind: 'enrich', workflowOwned: true, expectedRevision: task.revision, taskId: task.id,
|
|
2098
|
+
originSessionId: sessionId, originWorkspaceId: task.originWorkspaceId,
|
|
2074
2099
|
rows: runtime.rows, headers: runtime.headers, fieldSelection: task.fieldSelection,
|
|
2075
2100
|
});
|
|
2076
2101
|
if (!response.command?.commandId || !response.command?.prompt) throw new Error('Host 未返回执行凭证;未发送任务。');
|
|
@@ -2079,6 +2104,10 @@ body:has(.dcAgentPromptBackdrop) { overflow: hidden; }
|
|
|
2079
2104
|
|
|
2080
2105
|
async function refreshHostedTask(actions, sessionId, taskId, run, navigate = true) {
|
|
2081
2106
|
const response = await requestJson(`/data-cleaning/api/workflow/tasks/${encodeURIComponent(taskId)}`);
|
|
2107
|
+
if (!taskBelongsToSession(response.task, sessionId)) {
|
|
2108
|
+
if (readTaskBindings()[sessionId] === taskId) bindTask(sessionId, null);
|
|
2109
|
+
return null;
|
|
2110
|
+
}
|
|
2082
2111
|
const selectedTask = workflowTaskBySession.get(String(sessionId));
|
|
2083
2112
|
if (selectedTask && selectedTask.id !== taskId) return response.task;
|
|
2084
2113
|
if (selectedTask && selectedTask.revision > response.task.revision) return selectedTask;
|
|
@@ -2136,6 +2165,7 @@ body:has(.dcAgentPromptBackdrop) { overflow: hidden; }
|
|
|
2136
2165
|
workflowContract: null,
|
|
2137
2166
|
workflowTask: null,
|
|
2138
2167
|
workflowTasks: [],
|
|
2168
|
+
historyTask: null,
|
|
2139
2169
|
taskTitle: '企业数据清洗补全任务',
|
|
2140
2170
|
mappings: [],
|
|
2141
2171
|
objectives: [...DEFAULT_CLEANING_KEYS],
|
|
@@ -2178,6 +2208,10 @@ body:has(.dcAgentPromptBackdrop) { overflow: hidden; }
|
|
|
2178
2208
|
draft.workflowTasks = (Array.isArray(value) ? value : []).map(task =>
|
|
2179
2209
|
(cached.get(task.id)?.revision ?? -1) > (task.revision ?? -1) ? cached.get(task.id) : task);
|
|
2180
2210
|
},
|
|
2211
|
+
setHistoryTask: (draft, value) => { draft.historyTask = value; },
|
|
2212
|
+
refreshHistoryTask: (draft, value) => {
|
|
2213
|
+
if (value?.id === draft.historyTask?.id && value.revision >= draft.historyTask.revision) draft.historyTask = value;
|
|
2214
|
+
},
|
|
2181
2215
|
setTaskTitle: (draft, value) => { draft.taskTitle = String(value ?? ''); },
|
|
2182
2216
|
setMappings: (draft, value) => {
|
|
2183
2217
|
const next = Array.isArray(value) ? value : [];
|
|
@@ -3751,6 +3785,7 @@ body:has(.dcAgentPromptBackdrop) { overflow: hidden; }
|
|
|
3751
3785
|
const workflowContract = useStore((state) => state.workflowContract);
|
|
3752
3786
|
const workflowTask = useStore((state) => state.workflowTask);
|
|
3753
3787
|
const workflowTasks = useStore((state) => state.workflowTasks);
|
|
3788
|
+
const historyTask = useStore((state) => state.historyTask);
|
|
3754
3789
|
const taskTitle = useStore((state) => state.taskTitle);
|
|
3755
3790
|
const mappings = useStore((state) => state.mappings);
|
|
3756
3791
|
const objectives = useStore((state) => state.objectives);
|
|
@@ -3811,6 +3846,7 @@ body:has(.dcAgentPromptBackdrop) { overflow: hidden; }
|
|
|
3811
3846
|
try {
|
|
3812
3847
|
const task = await refreshHostedTask(actions, activeSessionId, workflowTask.id, null, false);
|
|
3813
3848
|
if (disposed) return;
|
|
3849
|
+
if (!task) return;
|
|
3814
3850
|
if (['matching', 'enriching'].includes(task.state)) timer = setTimeout(poll, 2000);
|
|
3815
3851
|
else if (step === 'match' || step === 'enrich') actions.setStep(task.stage || step);
|
|
3816
3852
|
} catch (error) { if (!disposed) actions.setError(error.message); }
|
|
@@ -4046,8 +4082,10 @@ body:has(.dcAgentPromptBackdrop) { overflow: hidden; }
|
|
|
4046
4082
|
throw new Error('当前会话无法提交数据清洗补全企查查任务');
|
|
4047
4083
|
}
|
|
4048
4084
|
const latest = workflowTaskBySession.get(String(activeSessionId)) ?? cachedTask;
|
|
4085
|
+
if (!taskBelongsToSession(latest, activeSessionId)) throw new Error('此任务不属于当前会话。');
|
|
4049
4086
|
const prepared = await api('/data-cleaning/api/g5/commands', {
|
|
4050
4087
|
...payload, workflowOwned: true, expectedRevision: latest.revision,
|
|
4088
|
+
originSessionId: activeSessionId, originWorkspaceId: latest.originWorkspaceId,
|
|
4051
4089
|
...(payload.kind === 'enrich' ? {} : { confirmPaidCalls: true }),
|
|
4052
4090
|
});
|
|
4053
4091
|
const command = prepared?.command;
|
|
@@ -4200,28 +4238,12 @@ body:has(.dcAgentPromptBackdrop) { overflow: hidden; }
|
|
|
4200
4238
|
};
|
|
4201
4239
|
|
|
4202
4240
|
const resumeWorkflowTask = async (task) => {
|
|
4241
|
+
// History selection has its own state; never hydrate the current task runtime.
|
|
4242
|
+
actions.setHistoryTask(task);
|
|
4203
4243
|
try {
|
|
4204
|
-
|
|
4205
|
-
|
|
4206
|
-
if (task.qccRunId) {
|
|
4207
|
-
try {
|
|
4208
|
-
const response = await api(`/data-cleaning/api/g5/run/${encodeURIComponent(task.qccRunId)}`);
|
|
4209
|
-
actions.setQccRun(response);
|
|
4210
|
-
} catch { /* Expired run data does not prevent persisted artifact downloads. */ }
|
|
4211
|
-
}
|
|
4244
|
+
const response = await requestJson(`/data-cleaning/api/workflow/tasks/${encodeURIComponent(task.id)}`);
|
|
4245
|
+
actions.refreshHistoryTask(response.task);
|
|
4212
4246
|
} catch (error) { actions.setError(error.message); return; }
|
|
4213
|
-
workflowTaskBySession.set(String(activeSessionId || 'unassigned'), task);
|
|
4214
|
-
actions.setWorkflowTask(task);
|
|
4215
|
-
actions.setTaskTitle(task.title);
|
|
4216
|
-
actions.setMappings(task.mappings || []);
|
|
4217
|
-
actions.setObjectives(task.objectives || []);
|
|
4218
|
-
actions.setFieldSelection(task.fieldSelection || []);
|
|
4219
|
-
for (const [key, value] of Object.entries(task.matchRules || {})) actions.setMatchRule(key, value);
|
|
4220
|
-
actions.setStep(task.stage || 'upload');
|
|
4221
|
-
if (!runtimeFor(task.id, false)?.rows?.length && task.source?.rowCount && !(task.artifacts || []).length) {
|
|
4222
|
-
actions.setDataset(null);
|
|
4223
|
-
actions.setError('已恢复任务元数据。出于隐私保护,原始企业名单未持久化;继续处理前请重新上传源文件。');
|
|
4224
|
-
}
|
|
4225
4247
|
};
|
|
4226
4248
|
|
|
4227
4249
|
const startNewWorkflow = () => {
|
|
@@ -4251,6 +4273,7 @@ body:has(.dcAgentPromptBackdrop) { overflow: hidden; }
|
|
|
4251
4273
|
const rows = exportRows();
|
|
4252
4274
|
if (!rows?.length) throw new Error('当前页面没有可导出的明细。若任务来自历史记录,请重新上传源文件并继续处理。');
|
|
4253
4275
|
const response = await requestJson(`/data-cleaning/api/workflow/tasks/${encodeURIComponent(latest.id)}/artifacts`, 'POST', {
|
|
4276
|
+
originSessionId: activeSessionId, originWorkspaceId: latest.originWorkspaceId,
|
|
4254
4277
|
expectedRevision: latest.revision,
|
|
4255
4278
|
headers: runtime.headers,
|
|
4256
4279
|
rows,
|
|
@@ -4350,7 +4373,7 @@ body:has(.dcAgentPromptBackdrop) { overflow: hidden; }
|
|
|
4350
4373
|
let pane;
|
|
4351
4374
|
if (step === 'history') {
|
|
4352
4375
|
pane = h('div', { className: 'dcAgentPane' },
|
|
4353
|
-
h('p', { className: 'dcAgentHint' }, '
|
|
4376
|
+
h('p', { className: 'dcAgentHint' }, '当前 Profile 的任务历史,仅只读查看;不会改变当前会话的任务、名单、规则或执行范围。'),
|
|
4354
4377
|
h('div', { className: 'dcAgentRow' },
|
|
4355
4378
|
h('button', {
|
|
4356
4379
|
type: 'button',
|
|
@@ -4365,9 +4388,29 @@ body:has(.dcAgentPromptBackdrop) { overflow: hidden; }
|
|
|
4365
4388
|
onClick: startNewWorkflow,
|
|
4366
4389
|
}, '新建清洗任务'),
|
|
4367
4390
|
),
|
|
4391
|
+
historyTask ? h('section', { className: 'dcAgentSection', 'aria-label': '历史任务只读详情' },
|
|
4392
|
+
h('h3', null, workflowDisplayName(historyTask)),
|
|
4393
|
+
h('p', { className: 'dcAgentHint' }, workflowOriginLabel(historyTask)),
|
|
4394
|
+
h('p', null, `只读 · ${WORKFLOW_STATE_LABELS[historyTask.state] || '未知'} · ${historyTask.source?.rowCount ?? 0} 行`),
|
|
4395
|
+
h('p', { className: 'dcAgentHint' }, `任务编号:${historyTask.id}`),
|
|
4396
|
+
sessionWorkbenchController?.canOpenOrigin?.(historyTask)
|
|
4397
|
+
? h('button', { type: 'button', className: 'dcAgentButton', onClick: () => {
|
|
4398
|
+
try { sessionWorkbenchController.openOrigin(historyTask); }
|
|
4399
|
+
catch (error) { actions.setError(error.message); }
|
|
4400
|
+
} }, '打开来源会话')
|
|
4401
|
+
: h('p', { className: 'dcAgentHint' }, '来源会话未知、已不可用或宿主不支持打开;保留只读查看和已有文件下载。'),
|
|
4402
|
+
(historyTask.artifacts || []).filter(a => a.format === 'xlsx').map(artifact =>
|
|
4403
|
+
h('div', { key: artifact.id, className: 'dcAgentRow' },
|
|
4404
|
+
artifact.kind !== 'report' ? h('a', { className: 'dcAgentButton', href: `/data-cleaning/api/workflow/tasks/${encodeURIComponent(historyTask.id)}/artifacts/${encodeURIComponent(artifact.id)}?preview=1`, target: '_blank', rel: 'noopener' }, '预览 / 打开') : null,
|
|
4405
|
+
h('a', { className: artifact.kind === 'report' ? 'dcAgentHint' : 'dcAgentButton',
|
|
4406
|
+
href: `/data-cleaning/api/workflow/tasks/${encodeURIComponent(historyTask.id)}/artifacts/${encodeURIComponent(artifact.id)}`,
|
|
4407
|
+
download: artifact.fileName }, artifact.kind === 'report' ? '下载任务结果报告' : `下载 ${artifact.fileName}`))),
|
|
4408
|
+
!historyTask.artifacts?.length ? h('p', { className: 'dcAgentHint' }, '此任务暂无已保存制品;历史查看不会重新调用企查查。') : null,
|
|
4409
|
+
) : null,
|
|
4368
4410
|
workflowTasks.length ? h('div', { className: 'dcAgentPane', 'aria-label': '任务历史列表' },
|
|
4369
4411
|
workflowTasks.map((task) => h('button', { key: task.id, type: 'button', className: 'dcAgentHistoryTask', onClick: () => resumeWorkflowTask(task) },
|
|
4370
4412
|
h('h3', null, workflowDisplayName(task)),
|
|
4413
|
+
h('p', { className: 'dcAgentHint' }, workflowOriginLabel(task)),
|
|
4371
4414
|
h('div', { className: 'dcAgentRow' },
|
|
4372
4415
|
h('span', { className: 'dcAgentJobsPill', 'data-state': task.state ?? 'idle' }, WORKFLOW_STATE_LABELS[task.state] ?? task.state ?? '未知'),
|
|
4373
4416
|
h('span', { className: 'dcAgentHint' }, `${task.source?.rowCount ?? 0} 行 · ${task.updatedAt ? new Date(task.updatedAt).toLocaleString() : ''}`),
|
|
@@ -4966,6 +5009,27 @@ body:has(.dcAgentPromptBackdrop) { overflow: hidden; }
|
|
|
4966
5009
|
: null;
|
|
4967
5010
|
const disconnectFixture = typeof ctx.inject !== 'function' ? connectProvider(ctx.betterSidebar) : () => {};
|
|
4968
5011
|
const controller = {
|
|
5012
|
+
originFor(sessionId) {
|
|
5013
|
+
const sessions = typeof ctx.get === 'function' ? ctx.get('sessions') : ctx.sessions;
|
|
5014
|
+
const workspaces = typeof ctx.get === 'function' ? ctx.get('workspaces') : ctx.workspaces;
|
|
5015
|
+
const summary = sessions?.list?.getSnapshot?.()?.byId?.[sessionId];
|
|
5016
|
+
const workspace = workspaces?.list?.getSnapshot?.()?.items?.find(item => item.sessionIds?.includes(sessionId));
|
|
5017
|
+
return { originSessionId: sessionId, originWorkspaceId: workspace?.workspaceId || summary?.workspaceId || null,
|
|
5018
|
+
originSessionName: summary?.title || null, originWorkspaceName: workspace?.name || workspace?.title || null };
|
|
5019
|
+
},
|
|
5020
|
+
canOpenOrigin(task) {
|
|
5021
|
+
const sessions = typeof ctx.get === 'function' ? ctx.get('sessions') : ctx.sessions;
|
|
5022
|
+
if (!task?.originSessionId || !ownedSessionId(task.originSessionId) || typeof sessions?.open !== 'function') return false;
|
|
5023
|
+
const summary = sessions.list?.getSnapshot?.()?.byId?.[task.originSessionId];
|
|
5024
|
+
const scope = controller.originFor(task.originSessionId);
|
|
5025
|
+
return Boolean(summary && (!task.originWorkspaceId || scope.originWorkspaceId === task.originWorkspaceId));
|
|
5026
|
+
},
|
|
5027
|
+
openOrigin(task) {
|
|
5028
|
+
if (!controller.canOpenOrigin(task)) throw new Error('来源会话不可用;请继续只读查看。');
|
|
5029
|
+
const sessions = typeof ctx.get === 'function' ? ctx.get('sessions') : ctx.sessions;
|
|
5030
|
+
sessions.open(task.originSessionId);
|
|
5031
|
+
// Navigation only: no task binding, command preparation, or draft writes.
|
|
5032
|
+
},
|
|
4969
5033
|
storeFor: instanceFor,
|
|
4970
5034
|
actionsFor: sessionId => instanceFor(sessionId).actions,
|
|
4971
5035
|
open(sessionId, step) {
|
|
@@ -5168,6 +5232,10 @@ body:has(.dcAgentPromptBackdrop) { overflow: hidden; }
|
|
|
5168
5232
|
ensureWorkflowTask,
|
|
5169
5233
|
ensureEditableWorkflowTask,
|
|
5170
5234
|
queueWorkflowOperation,
|
|
5235
|
+
taskBelongsToSession,
|
|
5236
|
+
workflowOriginLabel,
|
|
5237
|
+
cacheWorkflowTask,
|
|
5238
|
+
refreshHostedTask,
|
|
5171
5239
|
};
|
|
5172
5240
|
return module.exports;
|
|
5173
5241
|
},
|
package/lib/image-workflow.js
CHANGED
|
@@ -23,6 +23,7 @@ export function createImageWorkflow({ getWorkflow, execution, commands }) {
|
|
|
23
23
|
summary: { total: rows.length, valid: rows.length, missingAnchor: 0,
|
|
24
24
|
duplicates: rows.length - new Set(result.entries).size } });
|
|
25
25
|
const payload = await execution.prepare({ taskId: task.id, expectedRevision: task.revision,
|
|
26
|
+
originSessionId: task.originSessionId, originWorkspaceId: task.originWorkspaceId,
|
|
26
27
|
kind: 'enrich', rows, headers: ['主体标识'], nameField: '主体标识' });
|
|
27
28
|
const command = commands.prepare(payload, { artifactOrigin: record.workflow.artifactOrigin });
|
|
28
29
|
record.qccCommand = { commandId: command.commandId, taskId: task.id };
|
package/lib/qcc-safety.js
CHANGED
|
@@ -78,6 +78,7 @@ export function safeAuditEvent(event) {
|
|
|
78
78
|
upstreamCode: source.upstreamCode ? String(source.upstreamCode) : null,
|
|
79
79
|
durationMs: Math.max(0, Number(source.durationMs ?? 0)),
|
|
80
80
|
};
|
|
81
|
+
if (['success-data', 'success-empty', 'not-required', 'no-permission', 'failed', 'unknown'].includes(source.providerState)) safe.providerState = source.providerState;
|
|
81
82
|
if (source.catalogVersion || Array.isArray(source.missing) || Array.isArray(source.unknown)) {
|
|
82
83
|
safe.catalogVersion = source.catalogVersion ? String(source.catalogVersion).slice(0, 64) : null;
|
|
83
84
|
safe.missing = safeCatalogLabels(source.missing);
|
package/lib/qcc.js
CHANGED
|
@@ -281,12 +281,26 @@ function normalizedFailure(result, toolName, state) {
|
|
|
281
281
|
|
|
282
282
|
function emitAudit(options, event) {
|
|
283
283
|
try {
|
|
284
|
-
options?.onAudit?.(safeAuditEvent(event
|
|
284
|
+
options?.onAudit?.(safeAuditEvent({ ...event, providerState: event.providerState ?? providerResultState({
|
|
285
|
+
error: event.outcome === 'failed' || event.outcome === 'not-dispatched' ? { code: event.code, upstreamCode: event.upstreamCode } : null,
|
|
286
|
+
}) }));
|
|
285
287
|
} catch {
|
|
286
288
|
// 审计 sink 失败不能改变计费工具调用的业务结果。
|
|
287
289
|
}
|
|
288
290
|
}
|
|
289
291
|
|
|
292
|
+
/** Provider outcome is separate from execution completion and business review. */
|
|
293
|
+
export function providerResultState({ data, error, required = true } = {}) {
|
|
294
|
+
if (!required) return 'not-required';
|
|
295
|
+
if (error) return ['401', '403', 'QCC_AUTH_REQUIRED'].includes(String(error.upstreamCode || error.code)) ? 'no-permission' : 'failed';
|
|
296
|
+
if (Array.isArray(data)) return data.length ? 'success-data' : 'success-empty';
|
|
297
|
+
if (!data || typeof data !== 'object' || !Object.keys(data).length) return 'unknown';
|
|
298
|
+
const lists = ['实际控制人信息', '受益所有人信息', 'items', 'records', '企业列表'].map(k => data[k]).filter(Array.isArray);
|
|
299
|
+
if (lists.length && lists.every(v => !v.length)) return data.has_more === true ? 'unknown' : 'success-empty';
|
|
300
|
+
// No fabricated zero count for a missing/null/unrecognised transport payload.
|
|
301
|
+
return 'success-data';
|
|
302
|
+
}
|
|
303
|
+
|
|
290
304
|
function candidateView(candidate) {
|
|
291
305
|
const item = isRecord(candidate) ? candidate : {};
|
|
292
306
|
return {
|
|
@@ -835,15 +849,17 @@ export class QccHostBridge {
|
|
|
835
849
|
} : {}),
|
|
836
850
|
});
|
|
837
851
|
if (result?.isError !== true) {
|
|
852
|
+
const data = decodeQccToolValue(result?.value);
|
|
838
853
|
emitAudit(options, {
|
|
839
854
|
toolName: activeToolName,
|
|
840
855
|
callId,
|
|
841
856
|
attempt,
|
|
842
857
|
outcome: 'success',
|
|
858
|
+
providerState: providerResultState({ data }),
|
|
843
859
|
durationMs: Date.now() - attemptStarted,
|
|
844
860
|
});
|
|
845
861
|
attemptAudited = true;
|
|
846
|
-
return { callId, toolName: activeToolName, data
|
|
862
|
+
return { callId, toolName: activeToolName, data };
|
|
847
863
|
}
|
|
848
864
|
const upstreamCode = upstreamFailureCode(result);
|
|
849
865
|
if (upstreamCode === 'UNKNOWN_TOOL' && attempt === 1 && !state.signal.aborted) {
|
package/lib/web.js
CHANGED
|
@@ -424,6 +424,7 @@ export function mountWebRoutes(wctx, { logger, report, TOOL_NAME, SKILL_NAME })
|
|
|
424
424
|
const payload = JSON.parse((await readBody(req)).toString('utf8'));
|
|
425
425
|
const record = imageIntake.require(decodeURIComponent(rest[0]));
|
|
426
426
|
const task = await (await getWorkflow()).require(payload.taskId);
|
|
427
|
+
await (await getWorkflow()).assertRequestOrigin(task.id, payload);
|
|
427
428
|
if (payload.executeOnSend !== true || record.state !== 'prepared' || task.state !== 'draft'
|
|
428
429
|
|| task.revision !== payload.expectedRevision || task.source?.type !== 'image'
|
|
429
430
|
|| task.source.fileName !== record.fileName || !task.fieldSelection.length) {
|
|
@@ -516,6 +517,7 @@ export function mountWebRoutes(wctx, { logger, report, TOOL_NAME, SKILL_NAME })
|
|
|
516
517
|
if (req.method === 'PATCH') {
|
|
517
518
|
const body = await readBody(req);
|
|
518
519
|
const payload = body.length ? JSON.parse(body.toString('utf8')) : {};
|
|
520
|
+
await workflow.assertRequestOrigin(taskId, payload);
|
|
519
521
|
return writeJson(res, 200, {
|
|
520
522
|
ok: true,
|
|
521
523
|
marker: 'data-cleaning-workflow-v2',
|
|
@@ -545,6 +547,7 @@ export function mountWebRoutes(wctx, { logger, report, TOOL_NAME, SKILL_NAME })
|
|
|
545
547
|
if (rest.length === 2 && req.method === 'POST') {
|
|
546
548
|
const body = await readBody(req);
|
|
547
549
|
const payload = body.length ? JSON.parse(body.toString('utf8')) : {};
|
|
550
|
+
await workflow.assertRequestOrigin(taskId, payload);
|
|
548
551
|
if (Number(payload.expectedRevision) !== current.revision) {
|
|
549
552
|
throw new WorkflowError('DC_WORKFLOW_REVISION_CONFLICT', 'Workflow task was updated by another session.', 409);
|
|
550
553
|
}
|
|
@@ -612,6 +615,7 @@ export function mountWebRoutes(wctx, { logger, report, TOOL_NAME, SKILL_NAME })
|
|
|
612
615
|
const body = await readBody(req);
|
|
613
616
|
const payload = body.length ? JSON.parse(body.toString('utf8')) : {};
|
|
614
617
|
const action = rest[2];
|
|
618
|
+
await workflow.assertRequestOrigin(taskId, payload);
|
|
615
619
|
const actions = {
|
|
616
620
|
upload: () => workflow.recordUpload(taskId, payload),
|
|
617
621
|
rules: () => workflow.confirmRules(taskId, payload),
|
package/lib/workflow-contract.js
CHANGED
|
@@ -215,5 +215,17 @@ export function assertWorkflowRecordShape(record) {
|
|
|
215
215
|
if (!STAGE_IDS.has(record.stage) || !STATE_IDS.has(record.state)) {
|
|
216
216
|
throw new WorkflowContractError('DC_WORKFLOW_SCHEMA', 'Workflow record contains an invalid stage or state.');
|
|
217
217
|
}
|
|
218
|
-
|
|
218
|
+
// Additive v2 migration: never infer ownership from whichever Session reads history.
|
|
219
|
+
return { ...record, ...normalizeWorkflowOrigin(record) };
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
export function normalizeWorkflowOrigin(value = {}) {
|
|
223
|
+
const id = value => typeof value === 'string' && /^[\w:.-]{1,200}$/.test(value) ? value : null;
|
|
224
|
+
const label = value => typeof value === 'string' ? value.replace(/[\u0000-\u001f\u007f]/g, '').trim().slice(0, 120) || null : null;
|
|
225
|
+
return {
|
|
226
|
+
originSessionId: id(value.originSessionId),
|
|
227
|
+
originWorkspaceId: id(value.originWorkspaceId),
|
|
228
|
+
originSessionName: label(value.originSessionName),
|
|
229
|
+
originWorkspaceName: label(value.originWorkspaceName),
|
|
230
|
+
};
|
|
219
231
|
}
|
|
@@ -10,7 +10,7 @@ export function createWorkflowExecution({ getWorkflow, artifacts }) {
|
|
|
10
10
|
return {
|
|
11
11
|
async prepare(payload) {
|
|
12
12
|
const store = await requireStore();
|
|
13
|
-
const task = await store.
|
|
13
|
+
const task = await store.assertRequestOrigin(payload.taskId, payload);
|
|
14
14
|
if (!['rules_confirmed', 'diagnosed', 'review_required', 'partial'].includes(task.state)) {
|
|
15
15
|
throw new WorkflowError('DC_EXECUTION_STATE', '请先在工作台确认规则;进行中或已完成任务不能重复启动。', 409);
|
|
16
16
|
}
|
package/lib/workflow.js
CHANGED
|
@@ -12,6 +12,7 @@ import {
|
|
|
12
12
|
assertWorkflowTransition,
|
|
13
13
|
normalizeFieldSelection,
|
|
14
14
|
normalizeWorkflowDraft,
|
|
15
|
+
normalizeWorkflowOrigin,
|
|
15
16
|
validateMappings,
|
|
16
17
|
} from './workflow-contract.js';
|
|
17
18
|
|
|
@@ -140,6 +141,7 @@ export class DataCleaningWorkflowStore {
|
|
|
140
141
|
const id = this.idFactory();
|
|
141
142
|
const record = {
|
|
142
143
|
id,
|
|
144
|
+
...normalizeWorkflowOrigin(input),
|
|
143
145
|
schemaVersion: WORKFLOW_SCHEMA_VERSION,
|
|
144
146
|
revision: 1,
|
|
145
147
|
title: draft.title,
|
|
@@ -180,6 +182,16 @@ export class DataCleaningWorkflowStore {
|
|
|
180
182
|
return record;
|
|
181
183
|
}
|
|
182
184
|
|
|
185
|
+
async assertRequestOrigin(id, input = {}) {
|
|
186
|
+
const task = await this.require(id);
|
|
187
|
+
// Origin-less legacy/headless records remain readable; UI never adopts them.
|
|
188
|
+
if (task.originSessionId && (input.originSessionId !== task.originSessionId
|
|
189
|
+
|| (task.originWorkspaceId && input.originWorkspaceId !== task.originWorkspaceId))) {
|
|
190
|
+
throw new WorkflowError('DC_WORKFLOW_SCOPE', '任务不属于此来源会话;历史任务仅可只读查看。', 409);
|
|
191
|
+
}
|
|
192
|
+
return task;
|
|
193
|
+
}
|
|
194
|
+
|
|
183
195
|
async mutate(id, expectedRevision, updater) {
|
|
184
196
|
const key = String(id);
|
|
185
197
|
const current = await this.require(key);
|
|
@@ -197,6 +209,7 @@ export class DataCleaningWorkflowStore {
|
|
|
197
209
|
...latest,
|
|
198
210
|
...patch,
|
|
199
211
|
id: latest.id,
|
|
212
|
+
...normalizeWorkflowOrigin(latest),
|
|
200
213
|
schemaVersion: WORKFLOW_SCHEMA_VERSION,
|
|
201
214
|
revision: latest.revision + 1,
|
|
202
215
|
updatedAt: this.nowFn(),
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-data-cleaning-agent",
|
|
3
|
-
"version": "0.9.
|
|
3
|
+
"version": "0.9.8",
|
|
4
4
|
"description": "Data cleaning and data enrichment for CSV/XLSX/JSON enterprise lists in DeepSeek Harness, including spreadsheet cleaning, deduplication, profiling, optional Qichacha MCP and exports.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "lib/index.js",
|
|
@@ -20,6 +20,7 @@
|
|
|
20
20
|
"install.sh",
|
|
21
21
|
"marketing",
|
|
22
22
|
"docs/USER-GUIDE.md",
|
|
23
|
+
"docs/RELEASE-0.9.8.md",
|
|
23
24
|
"docs/FIRST-CONTRIBUTION.md",
|
|
24
25
|
"docs/COMPATIBILITY.md",
|
|
25
26
|
"docs/UI-V1.5.0-ADOPTION.md",
|