@us_momo/dsh-erp-analysis 0.0.0-stage → 1.2.2

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/README.md CHANGED
@@ -1,3 +1,78 @@
1
- # Temporary Holding Version
2
-
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
1
+ # 公司 ERP 分析插件
2
+
3
+ 在 DeepSeek Harness(DSH)中使用本人 ERP 账号开展选品研究、经营、SKU、备货、广告和财务分析。插件提供统一的 12 套公司方法;业务数据由 ERP 按本人权限提供,模型由使用者在 DSH 中选择。
4
+
5
+ 当前版本:**1.2.2**。适用于 Windows DSH,最低版本 `0.2.1-rc.3`。
6
+
7
+ ## 从 npm 安装
8
+
9
+ 1. 在 DSH 打开“插件 → 添加插件”。
10
+ 2. 在“包名或地址”填写:
11
+
12
+ ```text
13
+ @us_momo/dsh-erp-analysis
14
+ ```
15
+
16
+ 3. 点击“安装”,安装完成后点击“立即启用”,按提示重启 DSH。
17
+ 4. 打开左侧“公司 ERP 分析”,点击“登录 ERP”,打开显示的链接并核对本人账号。浏览器账号不对时点击“换个账号登录”。
18
+ 5. 返回 DSH 刷新状态,确认当前账号后点击“开始公司分析”。
19
+
20
+ 如需固定安装本次版本,填写:
21
+
22
+ ```text
23
+ @us_momo/dsh-erp-analysis@1.2.2
24
+ ```
25
+
26
+ ERP 管理员须先为该账号开启“允许 DSH 接入”。安装插件本身不授予 ERP 数据权限。
27
+
28
+ 安装时可以在 DSH 的 Registry 选项中选择 npm 官方源或国内镜像。镜像可能晚于官方源同步新版本;插件包下载不需要 Git 或访问 GitHub。
29
+
30
+ ## 登录、切换与退出
31
+
32
+ 面板显示当前 ERP 账号,登录记住 30 天。同一人可以在多台电脑登录,电脑名称只用于识别登录来源。
33
+
34
+ - **切换账号**:自动退出当前电脑的旧授权,再提供新登录链接。登录新账号后,新建公司分析对话。
35
+ - **退出登录**:退出当前电脑的 ERP 授权,不影响其他电脑。
36
+ - 浏览器中的 ERP 会话独立;确认页面的“换个账号登录”可退出该浏览器会话,再沿原 ERP 入口登录。
37
+
38
+ 旧分析连接在切换后停止 ERP 调用。待共享报告保留原归属,不会以新账号上传;原有聊天和工作文件保留在本机。
39
+
40
+ ## 更新插件
41
+
42
+ 使用 npm 包名安装后,完成当前分析,在 **DSH Market → 已安装** 中刷新检查,发现新版时点击 **更新**,按提示重启 DSH。新建公司分析对话使用新版。该按钮由 DSH Market 提供;未使用 DSH Market 时,在 DSH 插件管理中卸载后重新安装同一 npm 包名。刚发布的版本可能需要等待镜像同步或 DSH 的新版本等待策略允许安装。
43
+
44
+ **旧版首次迁移**:从 GitHub、安装包直链或本地 `.tgz` 安装的旧公司插件,先在 DSH 中卸载一次,再用 `@us_momo/dsh-erp-analysis` 安装、立即启用并按提示重启。旧包名为 `@company/dsh-erp-analysis`,不能和新包同时启用。
45
+
46
+ 本机登录和工作资料位于 `%LOCALAPPDATA%/CompanyDSH`,与插件安装目录分开,正常更新和重新安装会保留。
47
+
48
+ ## GitHub 安装包备用入口
49
+
50
+ 也可以使用 GitHub Release 成品安装,不需要 Git:
51
+
52
+ ```text
53
+ https://github.com/Huazhufather/DSH_USEC/releases/latest/download/company-dsh-erp-analysis.tgz
54
+ ```
55
+
56
+ 安装包直链仍采用卸载后重新安装的手动更新方式。需要 DSH Market 一键更新时,使用上面的 npm 包名安装。
57
+
58
+ ## 公司方法与报告
59
+
60
+ 插件内含选品研究、经营复盘、SKU 策略、库存与供给、运输方案、广告复盘、产品成本、平台利润、平台结算、公司财务、现金安排及企业知识方法。
61
+
62
+ 正式报告在用户明确要求生成或保存后集中共享,已开启 DSH 接入的成员均可读取完整报告,包含成本和利润。普通聊天、草稿和临时附件保留本机。公司知识与共享报告从 ERP 读取,内容变化无需重新安装插件。
63
+
64
+ 选品方向、类目主报告与专题、候选货源、样品评价和研究原件,可在用户明确保存后写入 ERP“选品管理”。新对话读取最新档案继续研究,报告按章节更新并保留历史。机会由人判断,不产生采购、上架或实际付款。
65
+
66
+ ## 仓库内容
67
+
68
+ 本仓库提供可直接安装的插件成品:`package.json` 和插件入口位于根目录,`release.json` 内嵌固定版本的工具及方法资源,安装时无需在同事电脑构建。
69
+
70
+ 1.2.2 增加 npm 发布入口并接入 DSH Market 版本检查和更新,保留 1.2.1 的服务繁忙提示修复。
71
+
72
+ 1.2.0 新增选品研究与原件共享,保留固定安装地址、账号切换和本机资料。
73
+
74
+ 1.1.6 修复未安装 Git 的电脑无法安装的问题:主入口改为 GitHub Release 安装包直链,账号与业务方法沿用原实现。
75
+
76
+ ## 已安装 Git 的环境
77
+
78
+ 也可以填写 `github:Huazhufather/DSH_USEC` 从仓库安装。直接填写仓库地址或此快捷地址需要 Git;普通同事使用上方 npm 包名即可。
package/client.mjs ADDED
@@ -0,0 +1,258 @@
1
+ import { generateKeyPairSync, createHash, randomUUID, sign } from "node:crypto";
2
+ import { readFile, writeFile, mkdir, readdir, rename } from "node:fs/promises";
3
+ import { join, basename, dirname } from "node:path";
4
+ import { hostname, userInfo } from "node:os";
5
+ import { execFileSync } from "node:child_process";
6
+ import { trustedErpUrl, readJson, atomicJson } from "./release.mjs";
7
+ export async function secureDirectory(path) {
8
+ await mkdir(path, { recursive: true, mode: 0o700 });
9
+ if (process.platform === "win32") {
10
+ const output = execFileSync(
11
+ "C:/Windows/System32/whoami.exe",
12
+ ["/user", "/fo", "csv", "/nh"],
13
+ { encoding: "utf8", windowsHide: true },
14
+ );
15
+ const sid = output.match(/S-1-5-[0-9-]+/);
16
+ if (!sid) throw new Error("无法确认当前 Windows 用户。");
17
+ execFileSync(
18
+ "C:/Windows/System32/icacls.exe",
19
+ [path, "/inheritance:r", "/grant:r", `*${sid[0]}:(OI)(CI)F`],
20
+ { windowsHide: true, stdio: "pipe" },
21
+ );
22
+ }
23
+ }
24
+ export class TeamClient {
25
+ constructor(root, config) {
26
+ this.root = root;
27
+ this.config = config;
28
+ this.state = join(root, "private");
29
+ trustedErpUrl(config.server);
30
+ this.base = new URL("/api/v1/dsh/", config.server);
31
+ }
32
+ async key() {
33
+ await secureDirectory(this.state);
34
+ const file = join(this.state, "device-key.pem");
35
+ let privateKey;
36
+ try {
37
+ privateKey = await readFile(file, "utf8");
38
+ } catch (e) {
39
+ if (e.code !== "ENOENT") throw e;
40
+ privateKey = generateKeyPairSync("ed25519", {
41
+ publicKeyEncoding: { type: "spki", format: "pem" },
42
+ privateKeyEncoding: { type: "pkcs8", format: "pem" },
43
+ }).privateKey;
44
+ await writeFile(file, privateKey, { flag: "wx", mode: 0o600 });
45
+ }
46
+ return privateKey;
47
+ }
48
+ checkIdentity(device, expectedDeviceId) {
49
+ if ((expectedDeviceId && device.deviceId !== expectedDeviceId) ||
50
+ (this.config.pinIdentity && this.boundDeviceId && device.deviceId !== this.boundDeviceId))
51
+ throw new Error("ERP 账号已切换,请新建公司分析对话。原账号的待共享报告已保留,不会以当前账号上传。");
52
+ if (this.config.pinIdentity) this.boundDeviceId ||= device.deviceId;
53
+ }
54
+ async request(path, { method = "GET", body, signed = true, expectedDeviceId } = {}) {
55
+ const url = new URL(path, this.base);
56
+ if (
57
+ url.origin !== this.base.origin ||
58
+ !url.pathname.startsWith(this.base.pathname)
59
+ )
60
+ throw new Error("无效的公司接口。");
61
+ const raw =
62
+ body === undefined ? Buffer.alloc(0) : Buffer.from(JSON.stringify(body));
63
+ const headers = { "Content-Type": "application/json" };
64
+ if (signed) {
65
+ const device = await readJson(join(this.state, "device.json"), null);
66
+ if (!device) throw new Error("请先关联本人 ERP 账号,再重试本次操作。");
67
+ this.checkIdentity(device, expectedDeviceId);
68
+ const time = String(Math.floor(Date.now() / 1000)),
69
+ nonce = randomUUID();
70
+ const canonical = [
71
+ time,
72
+ nonce,
73
+ method,
74
+ url.pathname + url.search,
75
+ createHash("sha256").update(raw).digest("hex"),
76
+ ].join("\n");
77
+ headers["X-DSH-Device"] = device.deviceId;
78
+ headers["X-DSH-Time"] = time;
79
+ headers["X-DSH-Nonce"] = nonce;
80
+ headers["X-DSH-Signature"] = sign(
81
+ null,
82
+ Buffer.from(canonical),
83
+ await this.key(),
84
+ ).toString("base64");
85
+ }
86
+ let response;
87
+ try {
88
+ response = await fetch(url, {
89
+ method,
90
+ headers,
91
+ body: method === "GET" ? undefined : raw,
92
+ redirect: "error",
93
+ signal: AbortSignal.timeout(120000),
94
+ });
95
+ } catch {
96
+ throw new Error("公司服务暂时无法连接,请保留当前操作后重试。");
97
+ }
98
+ const chunks = [];
99
+ let size = 0;
100
+ for await (const chunk of response.body) {
101
+ size += chunk.length;
102
+ if (size > 32 * 1024 * 1024) throw new Error("服务响应超过限制。");
103
+ chunks.push(chunk);
104
+ }
105
+ let result;
106
+ try {
107
+ result = JSON.parse(Buffer.concat(chunks).toString("utf8"));
108
+ } catch {
109
+ throw new Error("公司服务返回了无效响应。");
110
+ }
111
+ if (!response.ok) {
112
+ const code = result.error?.code || `HTTP_${response.status}`;
113
+ const requestId = typeof result.error?.requestId === "string" &&
114
+ /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(result.error.requestId)
115
+ ? result.error.requestId : undefined;
116
+ const message = response.status === 503 || code === "SERVICE_UNAVAILABLE"
117
+ ? "公司服务繁忙,请稍后重试。"
118
+ : `公司服务拒绝本次操作 (${code})`;
119
+ const e = new Error(message + (requestId ? `(请求编号:${requestId})` : ""));
120
+ e.code = code;
121
+ e.status = response.status;
122
+ e.requestId = requestId;
123
+ throw e;
124
+ }
125
+ return result;
126
+ }
127
+ async connect(deviceName = hostname()) {
128
+ const previous = await readJson(join(this.state, "device.json"), null);
129
+ if (previous) {
130
+ const status = await this.status();
131
+ if (status.status === "active" || status.canRenew)
132
+ throw new Error("本机已登录 ERP,请使用续期或点击“切换账号”。");
133
+ if (status.status === "pending")
134
+ return {deviceId:previous.deviceId, expiresAt:status.expiresAt,
135
+ verificationUrl:new URL(`connect/${encodeURIComponent(previous.deviceId)}`,this.base).href};
136
+ if (status.status !== "revoked" || status.linked && !status.revoked)
137
+ throw new Error("尚未确认旧关联已解除,请先核对状态。");
138
+ }
139
+ const { createPublicKey } = await import("node:crypto");
140
+ const publicKey = createPublicKey(await this.key()).export({
141
+ type: "spki",
142
+ format: "pem",
143
+ });
144
+ const result = await this.request("devices/request", {
145
+ method: "POST",
146
+ signed: false,
147
+ body: { publicKey, deviceName },
148
+ });
149
+ await atomicJson(join(this.state, "device.json"), {
150
+ deviceId: result.deviceId,
151
+ });
152
+ const verification = trustedErpUrl(result.verificationUrl);
153
+ if (verification.origin !== this.base.origin)
154
+ throw new Error("确认地址与公司 ERP 不一致。");
155
+ return {
156
+ deviceId: result.deviceId,
157
+ verificationUrl: verification.href,
158
+ expiresAt: result.expiresAt,
159
+ };
160
+ }
161
+ async status() {
162
+ const device = await readJson(join(this.state, "device.json"), null);
163
+ if (!device) return { status: "unpaired" };
164
+ return this.request(
165
+ `devices/${encodeURIComponent(device.deviceId)}/status`,
166
+ { method: "POST", body: {} },
167
+ );
168
+ }
169
+ async logout() {
170
+ const device = await readJson(join(this.state, "device.json"), null);
171
+ if (device) {
172
+ const result = await this.request(`devices/${encodeURIComponent(device.deviceId)}/revoke`, {
173
+ method: "POST", body: {}, expectedDeviceId: device.deviceId,
174
+ });
175
+ if (result.status !== "revoked" || result.deviceId !== device.deviceId)
176
+ throw new Error("尚未确认当前账号已退出,请刷新状态后重试。");
177
+ await atomicJson(join(this.state, "device.json"), null);
178
+ }
179
+ return { status: "unpaired" };
180
+ }
181
+ async switchAccount() {
182
+ await this.logout();
183
+ return this.connect();
184
+ }
185
+ async tools() {
186
+ return this.request("tools");
187
+ }
188
+ async call(name, args = {}, { expectedDeviceId } = {}) {
189
+ return this.request("tools/call", {
190
+ method: "POST",
191
+ body: { name, arguments: args },
192
+ expectedDeviceId,
193
+ });
194
+ }
195
+ async saveReport(report) {
196
+ await secureDirectory(this.state);
197
+ const device = await readJson(join(this.state, "device.json"), null);
198
+ if (!device) throw new Error("请先关联本人 ERP 账号,再保存共享报告。");
199
+ this.checkIdentity(device);
200
+ const queue = join(this.state, "reports", device.deviceId);
201
+ await mkdir(queue, { recursive: true, mode: 0o700 });
202
+ const operationId = report.operationId || randomUUID();
203
+ if (
204
+ !/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(
205
+ operationId,
206
+ )
207
+ )
208
+ throw new Error("报告操作编号必须是 UUID。");
209
+ const path = join(queue, operationId + ".json");
210
+ const pending = await readJson(path, null);
211
+ const original = pending || await readJson(path + ".shared", null);
212
+ const request = { ...report, operationId };
213
+ const generatedAt = original ? original.source?.generatedAt : new Date().toISOString();
214
+ if (generatedAt && report.source?.generatedAt === undefined)
215
+ request.source = { ...report.source, generatedAt };
216
+ if (original && JSON.stringify(original) !== JSON.stringify(request))
217
+ throw new Error("同一报告操作编号不能修改内容。");
218
+ if (!pending) await atomicJson(path, request);
219
+ return this.sendQueued(path, request);
220
+ }
221
+ async sendQueued(path, request) {
222
+ try {
223
+ const receipt = await this.call("company_reports_save", { request }, { expectedDeviceId: basename(dirname(path)) });
224
+ if (receipt.status !== "shared")
225
+ throw new Error("服务器尚未确认报告共享。");
226
+ await rename(path, path + ".shared");
227
+ return { ...receipt, operationId: request.operationId };
228
+ } catch (e) {
229
+ return {
230
+ status: "pending",
231
+ operationId: request.operationId,
232
+ message: e.message,
233
+ };
234
+ }
235
+ }
236
+ async flushReports() {
237
+ const device = await readJson(join(this.state, "device.json"), null);
238
+ if (!device) throw new Error("请先关联本人 ERP 账号,再保存共享报告。");
239
+ this.checkIdentity(device);
240
+ const queue = join(this.state, "reports", device.deviceId);
241
+ await mkdir(queue, { recursive: true });
242
+ const results = [];
243
+ for (const owner of await readdir(join(this.state, "reports"), { withFileTypes: true })) {
244
+ if (!owner.isDirectory() || owner.name === device.deviceId) continue;
245
+ const files = await readdir(join(this.state, "reports", owner.name));
246
+ const count = files.filter(name => name.endsWith(".json")).length;
247
+ if (count) results.push({status:"pending", reason:"different_device", count,
248
+ message:"原关联的待共享报告已保留,未以当前用户上传。"});
249
+ }
250
+ for (const name of await readdir(queue)) {
251
+ if (name.endsWith(".json")) {
252
+ const path = join(queue, name);
253
+ results.push(await this.sendQueued(path, await readJson(path)));
254
+ }
255
+ }
256
+ return results;
257
+ }
258
+ }
@@ -0,0 +1 @@
1
+ {"server": "http://211.97.68.49:10020"}
package/package.json CHANGED
@@ -1,6 +1,47 @@
1
- {
2
- "name": "@us_momo/dsh-erp-analysis",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
1
+ {
2
+ "name": "@us_momo/dsh-erp-analysis",
3
+ "version": "1.2.2",
4
+ "description": "公司 ERP 分析:选品研究、个人账号关联、统一方法、共享知识与报告。",
5
+ "type": "module",
6
+ "files": [
7
+ "client.mjs",
8
+ "company-public.json",
9
+ "plugin/",
10
+ "release.json",
11
+ "release.mjs"
12
+ ],
13
+ "publishConfig": {
14
+ "access": "public",
15
+ "registry": "https://registry.npmjs.org/"
16
+ },
17
+ "engines": {
18
+ "dsh": ">=0.2.1-rc.3 <0.3.0"
19
+ },
20
+ "exports": {
21
+ ".": "./plugin/index.mjs",
22
+ "./client": "./plugin/client.js",
23
+ "./locale/*.json": "./plugin/locale/*.json",
24
+ "./icon": "./plugin/icon.svg"
25
+ },
26
+ "dsh": {
27
+ "bundle": {
28
+ "patch": "./plugin/cordis.patch.yml"
29
+ },
30
+ "client": {
31
+ "platform": "web",
32
+ "immediately": true,
33
+ "inject": [
34
+ "@deepseek-ai/dsh-client-connection",
35
+ "@deepseek-ai/dsh-client-locale",
36
+ "@deepseek-ai/dsh-client-ui-layout",
37
+ "@deepseek-ai/dsh-client-ui-sidebar",
38
+ "@deepseek-ai/dsh-client-ui-workspace"
39
+ ]
40
+ }
41
+ },
42
+ "repository": {
43
+ "type": "git",
44
+ "url": "git+https://github.com/Huazhufather/DSH_USEC.git"
45
+ },
46
+ "homepage": "https://github.com/Huazhufather/DSH_USEC#readme"
47
+ }
@@ -0,0 +1,185 @@
1
+ window.__ModuleLoader__.load({
2
+ id: "@us_momo/dsh-erp-analysis",
3
+ factory(require) {
4
+ const React = require("react"), h = React.createElement;
5
+ const words = {
6
+ zh: {
7
+ title: "公司 ERP 分析", intro: "经营复盘、SKU、备货和财务分析,自动选用公司方法。",
8
+ refresh: "刷新状态", connect: "登录 ERP", continue: "继续登录", renew: "续期登录",
9
+ switchAccount: "切换账号", logout: "退出登录", signedOut: "已退出当前电脑的 ERP 登录。", accountChanged: "请在 ERP 确认新账号;登录后新建公司分析对话。",
10
+ authorize: "打开 ERP 登录并确认", awaiting: "在 ERP 完成确认后,返回这里刷新状态。",
11
+ unpaired: "先登录本人的 ERP 账号,再开始公司分析。", active: "当前账号:", expired: "登录已到期或权限已变更,请续期或切换账号。",
12
+ revoked: "当前登录已退出,可以重新登录。", pending: "等待本人在 ERP 页面确认登录。",
13
+ unavailable: "公司服务暂时无法连接,请稍后刷新。", loading: "正在读取公司接入状态…",
14
+ start: "开始公司分析", account: "个人 ERP 账号", devices: "我的设备", revoke: "解除此设备",
15
+ revokedDevice: "已解除", expires: "有效期至", version: "公司方法版本", needReload: "新版已准备好,完成当前任务后重启 DSH。",
16
+ methods: "使用统一公司方法;模型由你在 DSH 中自行选择。",
17
+ reports: "共享正式报告", loadReports: "读取共享报告", retry: "恢复待共享报告", queue: "份报告待共享",
18
+ sharing: "生成或保存正式报告后自动共享,所有已启用成员均可查看完整内容,包括成本和利润。聊天、草稿和临时附件仅保存在本机。",
19
+ noReports: "暂无共享报告。", reportTitle: "报告", author: "作者", reportVersion: "版本", time: "生成时间", download: "下载",
20
+ previousPage: "上一页", nextPage: "下一页", page: "页码",
21
+ retryDone: "已恢复共享报告", local: "本机公司资料目录", update: "安装更新",
22
+ updateHelp: "完成当前分析后,在 DSH Market 的已安装插件中刷新检查,发现新版时点击“更新”,按提示重启 DSH 并新建公司分析对话。旧 GitHub 或本地安装包首次迁移时,先卸载公司插件,再用 @us_momo/dsh-erp-analysis 安装并启用;ERP 登录和本机资料保留。",
23
+ failed: "操作未完成,请重试。", mismatch: "报告文件校验失败,请重新下载。", refreshing: "正在办理…",
24
+ },
25
+ en: {
26
+ title: "Company ERP Analysis", intro: "Business reviews, SKU, supply and finance analysis using company methods.",
27
+ refresh: "Refresh", connect: "Sign in to ERP", continue: "Continue sign-in", renew: "Renew sign-in",
28
+ switchAccount: "Switch account", logout: "Sign out", signedOut: "Signed out of ERP on this computer.", accountChanged: "Confirm the new ERP account, then start a new company analysis conversation.",
29
+ authorize: "Sign in and confirm in ERP", awaiting: "Confirm in ERP, then return here and refresh.",
30
+ unpaired: "Sign in with your own ERP account to begin.", active: "Current account:", expired: "Sign-in expired or permissions changed. Renew or switch accounts.",
31
+ revoked: "Signed out. You may sign in again.", pending: "Waiting for your ERP sign-in confirmation.",
32
+ unavailable: "The company service is unavailable. Refresh later.", loading: "Loading connection status…",
33
+ start: "Start company analysis", account: "Your ERP account", devices: "My devices", revoke: "Revoke device",
34
+ revokedDevice: "Revoked", expires: "Valid until", version: "Company method version", needReload: "An update is ready. Finish your work, then restart DSH.",
35
+ methods: "Company methods are managed centrally. Choose your own model in DSH.",
36
+ reports: "Shared formal reports", loadReports: "Load shared reports", retry: "Retry pending reports", queue: "reports pending upload",
37
+ sharing: "Formal reports are shared automatically with every enabled member, including costs and profit. Chats, drafts and temporary attachments stay on this computer.",
38
+ noReports: "No shared reports yet.", reportTitle: "Report", author: "Author", reportVersion: "Version", time: "Created", download: "Download",
39
+ previousPage: "Previous page", nextPage: "Next page", page: "Page",
40
+ retryDone: "Reports shared", local: "Local company workspace", update: "Install updates",
41
+ updateHelp: "Finish your analysis, refresh installed plugins in DSH Market, and select Update when a newer release is available. Restart DSH as prompted and start a new company analysis conversation. To migrate a GitHub or local archive installation, uninstall the company plugin once, then install and enable @us_momo/dsh-erp-analysis. ERP sign-in and local files are retained.",
42
+ failed: "The operation did not finish. Retry.", mismatch: "Report verification failed. Download again.", refreshing: "Working…",
43
+ },
44
+ };
45
+ const css = `.company-erp-panel{height:100%;overflow:auto;padding:36px clamp(20px,4vw,56px);color:var(--dsw-alias-label-primary,CanvasText);font:inherit;box-sizing:border-box}.company-erp-panel>div{max-width:1000px;margin:auto}.company-erp-panel h1{font-size:26px;line-height:1.3;margin:0 0 10px;font-weight:600}.company-erp-panel h2{font-size:18px;margin:0 0 12px;font-weight:600}.company-erp-panel p{line-height:1.7;max-width:72ch;margin:10px 0}.company-erp-panel header,.company-erp-panel .company-row{display:flex;gap:12px;align-items:center;flex-wrap:wrap}.company-erp-panel header{justify-content:space-between;align-items:flex-start}.company-erp-panel section{padding:28px 0;border-bottom:1px solid color-mix(in srgb,currentColor 18%,transparent)}.company-erp-panel button,.company-erp-panel a.company-button{font:inherit;min-height:38px;padding:8px 14px;border:1px solid color-mix(in srgb,currentColor 25%,transparent);border-radius:6px;background:transparent;color:inherit;cursor:pointer;text-decoration:none;display:inline-flex;align-items:center;box-sizing:border-box}.company-erp-panel button:hover,.company-erp-panel a.company-button:hover{background:color-mix(in srgb,currentColor 7%,transparent)}.company-erp-panel button.primary{background:#205eaa;border-color:#205eaa;color:#fff}.company-erp-panel button:disabled{opacity:.5;cursor:default}.company-erp-panel button:focus-visible,.company-erp-panel a:focus-visible{outline:2px solid #3e8ce0;outline-offset:3px}.company-erp-panel .company-message{min-height:28px;margin-top:16px;line-height:1.6}.company-erp-panel .company-error{color:light-dark(#a52420,#ffaaa4)}.company-erp-panel table{border-collapse:collapse;width:100%;font-size:14px}.company-erp-panel th,.company-erp-panel td{text-align:left;padding:12px 10px;border-bottom:1px solid color-mix(in srgb,currentColor 15%,transparent);overflow-wrap:anywhere}.company-erp-panel th{font-weight:600}.company-erp-panel .company-table{overflow-x:auto}.company-erp-panel small{font-size:13px}.company-erp-panel .company-path{overflow-wrap:anywhere}.company-erp-panel ::selection{background:#b7d6fa;color:#122b48}@media(max-width:700px){.company-erp-panel{padding:24px 18px}.company-erp-panel h1{font-size:23px}.company-erp-panel section{padding:22px 0}}`;
46
+ function Icon() {
47
+ return h("svg", { width: 18, height: 18, viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: 1.7, "aria-hidden": true },
48
+ h("path", { d: "M4 20V4h12v16M2 20h20M8 8h4M8 12h4M8 16h2M18 8h3v12" }));
49
+ }
50
+ return {
51
+ inject: ["connection", "slots", "locale", "layout", "uiWorkspace", "workspaces"],
52
+ apply(ctx) {
53
+ ctx.effect(() => ctx.locale.register("company-erp", words));
54
+ const t = ctx.locale.bind("company-erp");
55
+ async function api(action, body = {}) {
56
+ const response = await ctx.connection.rpc.call("/api", "company-erp/control", { action, body });
57
+ if (!response.ok) {
58
+ const error = new Error(response.error?.message || t("failed"));
59
+ error.code = response.error?.code; error.status = response.error?.details?.status;
60
+ throw error;
61
+ }
62
+ return response.value;
63
+ }
64
+ function CompanyPanel() {
65
+ const [state, setState] = React.useState(null), [busy, setBusy] = React.useState(false);
66
+ const [message, setMessage] = React.useState(""), [error, setError] = React.useState(false);
67
+ const [verification, setVerification] = React.useState(null), [devices, setDevices] = React.useState(null);
68
+ const [reports, setReports] = React.useState(null);
69
+ const [reportPage, setReportPage] = React.useState(1), [hasMoreReports, setHasMoreReports] = React.useState(false);
70
+ const alive = React.useRef(true), accountEpoch = React.useRef(0);
71
+ const refresh = async () => {
72
+ const epoch = accountEpoch.current;
73
+ const next = await api("status");
74
+ if (alive.current && epoch === accountEpoch.current) {
75
+ setState(next);
76
+ if (next.connection.status !== "active") { setReports(null); setDevices(null); }
77
+ }
78
+ return next;
79
+ };
80
+ React.useEffect(() => {
81
+ alive.current = true;
82
+ refresh().catch((e) => { if (alive.current) { setMessage(e.message); setError(true); } });
83
+ return () => { alive.current = false; };
84
+ }, []);
85
+ async function action(fn) {
86
+ setBusy(true); setError(false); setMessage(t("refreshing"));
87
+ try { await fn(); }
88
+ catch (e) { if (alive.current) {
89
+ if (e.status === 401 || e.status === 403) {
90
+ setReports(null); setDevices(null); setVerification(null);
91
+ setState((previous) => previous && { ...previous, connection: { status: "unavailable", message: e.message } });
92
+ }
93
+ setError(true); setMessage(e.message);
94
+ } }
95
+ finally { if (alive.current) setBusy(false); }
96
+ }
97
+ const connection = state?.connection, active = connection?.status === "active";
98
+ const canRenew = connection?.canRenew;
99
+ const user = connection?.user?.login && connection.user.login !== connection.user.displayName
100
+ ? `${connection.user.displayName} (${connection.user.login})`
101
+ : connection?.user?.login || connection?.user?.displayName || "";
102
+ const status = !connection ? t("loading") : active
103
+ ? `${t("active")} ${user}${connection.expiresAt ? ` · ${t("expires")} ${new Date(connection.expiresAt).toLocaleString()}` : ""}`
104
+ : connection.message || t(canRenew ? "expired" : connection.status) || t("unavailable");
105
+ function button(label, fn, disabled = false, primary = false) {
106
+ return h("button", { type: "button", disabled: busy || disabled, className: primary ? "primary" : undefined,
107
+ onClick: () => action(fn) }, label);
108
+ }
109
+ async function link(renew) {
110
+ const result = await api(renew ? "renew" : "connect");
111
+ setVerification(result.verificationUrl); setMessage(t("awaiting")); await refresh();
112
+ }
113
+ async function changeAccount(switching) {
114
+ accountEpoch.current++;
115
+ setReports(null); setDevices(null); setVerification(null); setState(null);
116
+ setReportPage(1); setHasMoreReports(false);
117
+ try {
118
+ const result = await api(switching ? "switch" : "logout");
119
+ setVerification(result.verificationUrl || null);
120
+ setMessage(t(switching ? "accountChanged" : "signedOut"));
121
+ } finally { await refresh(); }
122
+ }
123
+ async function loadReports(page = 1) {
124
+ const result = await api("reports", { page });
125
+ setReports(result.items || []); setReportPage(result.page);
126
+ setHasMoreReports(result.hasMore); setMessage("");
127
+ }
128
+ async function download(report) {
129
+ const file = await api("report", { reportId: report.reportId || report.id, version: report.version });
130
+ const bytes = Uint8Array.from(atob(file.contentBase64), (c) => c.charCodeAt(0));
131
+ const digest = Array.from(new Uint8Array(await crypto.subtle.digest("SHA-256", bytes)), (b) => b.toString(16).padStart(2, "0")).join("");
132
+ if (digest !== file.sha256) throw new Error(t("mismatch"));
133
+ const url = URL.createObjectURL(new Blob([bytes], { type: "application/octet-stream" }));
134
+ const link = document.createElement("a"); link.href = url; link.download = file.filename.split(/[\\/]/).pop(); link.click();
135
+ setTimeout(() => URL.revokeObjectURL(url), 1000); setMessage("");
136
+ }
137
+ return h("main", { className: "company-erp-panel" }, h("style", null, css), h("div", null,
138
+ h("header", null, h("div", null, h("h1", null, t("title")), h("p", null, t("intro"))),
139
+ button(t("refresh"), async () => { await refresh(); setMessage(""); })),
140
+ h("div", { role: error ? "alert" : "status", "aria-live": "polite", className: `company-message${error ? " company-error" : ""}` }, message),
141
+ h("section", null, h("h2", null, t("account")), h("p", null, status),
142
+ h("div", { className: "company-row" },
143
+ button(connection?.status === "pending" ? t("continue") : t("connect"), () => link(false), active || canRenew || !state || connection?.status === "unavailable"),
144
+ button(t("renew"), () => link(true), !active && !canRenew),
145
+ button(t("switchAccount"), () => changeAccount(true), !connection?.deviceId),
146
+ button(t("logout"), () => changeAccount(false), !connection?.deviceId),
147
+ button(t("start"), async () => {
148
+ const workspace = await ctx.workspaces.create({ path: state.workspace });
149
+ if (workspace.title === "workspace") await ctx.workspaces.rename(workspace.workspaceId, t("title"));
150
+ await ctx.uiWorkspace.openWorkspace(workspace.workspaceId, undefined, { agentPreset: "company-erp-analysis" });
151
+ setMessage("");
152
+ }, !active || state?.pendingReload, true)),
153
+ verification ? h("p", null, h("a", { className: "company-button", href: verification, target: "_blank", rel: "noreferrer noopener" }, t("authorize"))) : null,
154
+ h("p", null, h("small", null, `${t("version")} ${state?.version || "—"} · ${t("methods")}`)),
155
+ state?.pendingReload ? h("p", { role: "status" }, t("needReload")) : null),
156
+ h("section", null, h("h2", null, t("reports")), h("p", null, t("sharing")),
157
+ h("div", { className: "company-row" },
158
+ button(t("loadReports"), () => loadReports(), !active),
159
+ button(t("retry"), async () => { const r = await api("retry"); await refresh(); setMessage(`${t("retryDone")}: ${r.receipts.filter((x) => x.status === "shared").length}`); }, !active),
160
+ state?.pendingReports ? h("span", null, `${state.pendingReports} ${t("queue")}`) : null),
161
+ reports ? reports.length ? h("div", { className: "company-table" }, h("table", null,
162
+ h("thead", null, h("tr", null, ...["reportTitle", "author", "reportVersion", "time", "download"].map((key) => h("th", { key, scope: "col" }, t(key))))),
163
+ h("tbody", null, ...reports.map((report) => h("tr", { key: report.reportId || report.id },
164
+ h("td", null, report.title), h("td", null, report.authorName || report.author?.name || "—"),
165
+ h("td", null, report.version || report.currentVersion || "—"),
166
+ h("td", null, report.generatedAt ? new Date(report.generatedAt).toLocaleString() : "—"),
167
+ h("td", null, button(t("download"), () => download(report)))))))) : h("p", null, t("noReports")) : null,
168
+ reports && h("div", { className: "company-row" },
169
+ button(t("previousPage"), () => loadReports(reportPage - 1), reportPage <= 1 || !active),
170
+ h("span", { "aria-live": "polite" }, `${t("page")} ${reportPage}`),
171
+ button(t("nextPage"), () => loadReports(reportPage + 1), !hasMoreReports || !active))),
172
+ h("section", null, h("h2", null, t("devices")),
173
+ button(t("devices"), async () => { const r = await api("devices"); setDevices(r.items || r.devices || []); setMessage(""); }, !active),
174
+ devices?.map((device) => h("div", { className: "company-row", key: device.deviceId || device.id },
175
+ h("p", null, device.deviceName || device.name, " · ", device.revoked ? t("revokedDevice") : `${t("expires")} ${new Date(device.expiresAt).toLocaleString()}`),
176
+ button(t("revoke"), async () => { await api("revoke", { deviceId: device.deviceId || device.id }); await refresh(); setDevices(null); setReports(null); setVerification(null); setMessage(""); }, device.revoked)))),
177
+ h("section", null, h("h2", null, t("update")), h("p", null, t("updateHelp")),
178
+ h("p", null, h("small", null, t("local")), h("br"), h("span", { className: "company-path" }, state?.workspace || "—")))));
179
+ }
180
+ ctx.slots.inject("main", () => ctx.slots.register({ name: "main", key: "company-erp-analysis" }, CompanyPanel));
181
+ ctx.slots.inject("sidebar.panellist", () => ctx.slots.register({ name: "sidebar.panellist", id: "company-erp-analysis", order: 5, label: () => t("title") }, Icon));
182
+ },
183
+ };
184
+ },
185
+ });
@@ -0,0 +1,3 @@
1
+ - insert:
2
+ - id: company-erp-analysis
3
+ name: '@us_momo/dsh-erp-analysis'
@@ -0,0 +1 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 32 32" fill="none" stroke="#2765b8" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M6 25V7h14v18M4 25h24M10 11h6M10 15h6M10 19h3M21 12h5v13"/><path d="m17 22 4-4 3 2 4-6"/></svg>
@@ -0,0 +1,27 @@
1
+ import { prepareCompany, companyPreset, companyActions } from "./runtime.mjs";
2
+ export const inject = ["agentPresets", "connection"];
3
+ export async function apply(ctx) {
4
+ const runtime = await prepareCompany();
5
+ const dispose = await ctx.agentPresets.register(await companyPreset(runtime));
6
+ ctx.effect(() => dispose);
7
+ const action = companyActions(runtime);
8
+ // Connection owns browser authentication, request trust, owner access and Desktop IPC.
9
+ ctx.connection.fetch.register({
10
+ path: "/api/company-erp/control", methods: ["POST"], requestBody: "buffered",
11
+ async fetch(request) {
12
+ const message = await request.json();
13
+ if (message?.type !== "client-request" || message.method !== "company-erp/control" ||
14
+ typeof message.rpcId !== "string" || typeof message.payload?.action !== "string")
15
+ return new Response("Invalid company request", { status: 400 });
16
+ let result;
17
+ try {
18
+ request.signal.throwIfAborted();
19
+ result = { ok: true, value: await action(message.payload.action, message.payload.body) };
20
+ } catch (error) {
21
+ result = { ok: false, error: { code: error.code || "company/action-failed", message: error.message,
22
+ details: Number.isInteger(error.status) ? { status: error.status } : {} } };
23
+ }
24
+ return Response.json({ type: "server-response", rpcId: message.rpcId, result });
25
+ },
26
+ });
27
+ }
@@ -0,0 +1 @@
1
+ {"meta":{"title":"Company ERP Analysis","description":"Connect your ERP identity to company methods, knowledge and shared reports."}}
@@ -0,0 +1 @@
1
+ {"meta":{"title":"公司 ERP 分析","description":"关联本人 ERP,使用统一专家方法、公司知识和共享正式报告。"}}