@jayyuen66/dsh-ocr-review 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +233 -0
- package/client.js +1041 -0
- package/cordis.patch.yml +15 -0
- package/host.js +2936 -0
- package/icon.svg +1 -0
- package/package.json +112 -0
- package/scripts/get-cred.mjs +182 -0
package/icon.svg
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
<svg width="36" height="36" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><g fill="none" stroke="#658EFF" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"><path stroke="#54ECE7" stroke-width="5" d="m8 7 -4 4 4 4" opacity=".2"/><path stroke="#54ECE7" stroke-width="5" d="m14 7 4 4 -4 4" opacity=".2"/><path stroke="#54ECE7" stroke-width="5" d="m9 19 2 2 4 -4" opacity=".2"/><path stroke="#658EFF" d="m8 7 -4 4 4 4"/><path stroke="#658EFF" d="m14 7 4 4 -4 4"/><path stroke="#658EFF" d="m9 19 2 2 4 -4"/></g></svg>
|
package/package.json
ADDED
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@jayyuen66/dsh-ocr-review",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "dsh 工作区代码审查插件:把 open-code-review(ocr CLI)封装为 5 个模型工具 + 设置卡片(从 settings.yaml 选 provider/model 应用到 OCR 配置)。",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"author": "JayYuen",
|
|
7
|
+
"repository": {
|
|
8
|
+
"type": "git",
|
|
9
|
+
"url": "git+https://github.com/JayYuen666/dsh-ocr-review.git"
|
|
10
|
+
},
|
|
11
|
+
"files": [
|
|
12
|
+
"icon.svg",
|
|
13
|
+
"host.js",
|
|
14
|
+
"client.js",
|
|
15
|
+
"cordis.patch.yml",
|
|
16
|
+
"scripts"
|
|
17
|
+
],
|
|
18
|
+
"type": "module",
|
|
19
|
+
"main": "host.js",
|
|
20
|
+
"exports": {
|
|
21
|
+
".": "./host.js",
|
|
22
|
+
"./client": "./client.js",
|
|
23
|
+
"./cordis.patch.yml": "./cordis.patch.yml",
|
|
24
|
+
"./package.json": "./package.json"
|
|
25
|
+
},
|
|
26
|
+
"publishConfig": {
|
|
27
|
+
"access": "public",
|
|
28
|
+
"registry": "https://registry.npmjs.org"
|
|
29
|
+
},
|
|
30
|
+
"dependencies": {
|
|
31
|
+
"@deepseek-ai/dsh-atomic-write": "0.2.0-rc.2",
|
|
32
|
+
"@deepseek-ai/dsh-credentials": "0.2.0-rc.2",
|
|
33
|
+
"@deepseek-ai/dsh-home-paths": "0.2.0-rc.2",
|
|
34
|
+
"@deepseek-ai/schemastery": "3.18.4",
|
|
35
|
+
"@jayyuen66/dsh-plugin-shared": "^0.1.0",
|
|
36
|
+
"js-yaml": "^5.4.2"
|
|
37
|
+
},
|
|
38
|
+
"devDependencies": {
|
|
39
|
+
"@deepseek-ai/cordis": "4.0.4",
|
|
40
|
+
"@deepseek-ai/dsh-agent": "0.2.0-rc.2",
|
|
41
|
+
"@deepseek-ai/dsh-api-remotes": "0.2.0-rc.2",
|
|
42
|
+
"@deepseek-ai/dsh-client-locale": "0.2.0-rc.2",
|
|
43
|
+
"@deepseek-ai/dsh-client-store": "0.2.0-rc.2",
|
|
44
|
+
"@deepseek-ai/dsh-client-ui-layout": "0.2.0-rc.2",
|
|
45
|
+
"@deepseek-ai/dsh-client-ui-plugin-manager": "0.2.0-rc.2",
|
|
46
|
+
"@deepseek-ai/dsh-client-ui-renderer": "0.2.0-rc.2",
|
|
47
|
+
"@deepseek-ai/dsh-client-ui-settings": "0.2.0-rc.2",
|
|
48
|
+
"@deepseek-ai/dsh-client-ui-slots": "0.2.0-rc.2",
|
|
49
|
+
"@deepseek-ai/dsh-host-webserver": "0.2.0-rc.2",
|
|
50
|
+
"@deepseek-ai/dsh-jobs": "0.2.0-rc.2",
|
|
51
|
+
"@deepseek-ai/dsh-jobs-local": "0.2.0-rc.2",
|
|
52
|
+
"@deepseek-ai/dsh-package-manifest": "0.2.0-rc.2",
|
|
53
|
+
"@deepseek-ai/dsh-ptc-runtime": "0.2.0-rc.2",
|
|
54
|
+
"@deepseek-ai/dsh-sandbox": "0.2.0-rc.2",
|
|
55
|
+
"@deepseek-ai/dsh-scope": "0.2.0-rc.2",
|
|
56
|
+
"@deepseek-ai/dsh-session": "0.2.0-rc.2",
|
|
57
|
+
"@deepseek-ai/dsh-session-projection": "0.2.0-rc.2",
|
|
58
|
+
"@deepseek-ai/dsh-settings": "0.2.0-rc.2",
|
|
59
|
+
"@deepseek-ai/dsh-shell": "0.2.0-rc.2",
|
|
60
|
+
"@deepseek-ai/dsh-subprocess": "0.2.0-rc.2",
|
|
61
|
+
"@deepseek-ai/dsh-system-prompt": "0.2.0-rc.2",
|
|
62
|
+
"@deepseek-ai/dsh-tools": "0.2.0-rc.2",
|
|
63
|
+
"@deepseek-ai/dsh-user-approval": "0.2.0-rc.2",
|
|
64
|
+
"@types/node": "26.6.3",
|
|
65
|
+
"@types/react": "19.3.0",
|
|
66
|
+
"@vitest/coverage-v8": "5.0.2",
|
|
67
|
+
"eslint-plugin-sonarjs": "4.2.2",
|
|
68
|
+
"oxfmt": "0.71.0",
|
|
69
|
+
"oxlint": "1.86.0",
|
|
70
|
+
"oxlint-tsgolint": "7.0.2003",
|
|
71
|
+
"react": "^19.3.0",
|
|
72
|
+
"rolldown": "1.2.11",
|
|
73
|
+
"typescript": "7.0.2",
|
|
74
|
+
"vite": "8.3.1",
|
|
75
|
+
"vitest": "5.0.2"
|
|
76
|
+
},
|
|
77
|
+
"peerDependencies": {
|
|
78
|
+
"@deepseek-ai/dsh": "^0.2.0-rc.2"
|
|
79
|
+
},
|
|
80
|
+
"peerDependenciesMeta": {
|
|
81
|
+
"@deepseek-ai/dsh": {
|
|
82
|
+
"optional": true
|
|
83
|
+
}
|
|
84
|
+
},
|
|
85
|
+
"optionalDependencies": {
|
|
86
|
+
"@alibaba-group/open-code-review": "^1.12.11"
|
|
87
|
+
},
|
|
88
|
+
"icon": "icon.svg",
|
|
89
|
+
"engines": {
|
|
90
|
+
"dsh": ">=0.2.0-rc.2"
|
|
91
|
+
},
|
|
92
|
+
"dsh": {
|
|
93
|
+
"manifestVersion": 1,
|
|
94
|
+
"bundle": {
|
|
95
|
+
"patch": "./cordis.patch.yml"
|
|
96
|
+
},
|
|
97
|
+
"client": {
|
|
98
|
+
"platform": "web",
|
|
99
|
+
"immediately": true
|
|
100
|
+
}
|
|
101
|
+
},
|
|
102
|
+
"scripts": {
|
|
103
|
+
"build:client": "node build-client.mjs",
|
|
104
|
+
"build": "node build-host.mjs",
|
|
105
|
+
"typecheck": "tsc --noEmit -p tsconfig.json && tsc --noEmit -p tsconfig.client.json",
|
|
106
|
+
"lint": "oxlint .",
|
|
107
|
+
"test": "vitest run --coverage",
|
|
108
|
+
"test:watch": "vitest",
|
|
109
|
+
"check": "npm run --loglevel=warn typecheck && npm run --loglevel=warn lint && npm run --loglevel=warn build:client && npm run --loglevel=warn build && npm run --loglevel=warn test && npm run --loglevel=warn fmt:check",
|
|
110
|
+
"fmt:check": "oxfmt --check ."
|
|
111
|
+
}
|
|
112
|
+
}
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/// <reference types="node" />
|
|
3
|
+
// get-cred.mjs —— open-code-review 的 api_key_cmd 入口(本包唯一还在读宿主凭据
|
|
4
|
+
// 文件的地方,且**只读不问服务**)。
|
|
5
|
+
//
|
|
6
|
+
// 为什么这个脚本必须存在(官方通道改造后仍然保留):
|
|
7
|
+
// - dsh 侧的 provider 清单与 key 状态已经改走官方通道(ctx.settings.describe() /
|
|
8
|
+
// ctx.credentials),但 OCR 是**另一个进程树**里的外部 CLI:它只会按
|
|
9
|
+
// ~/.opencodereview/config.json 里的 api_key_cmd 起子进程取 key,够不到 cordis
|
|
10
|
+
// 容器,也没有 dsh 的依赖注入。api_key_cmd 就是它唯一的动态 key 通道。
|
|
11
|
+
// - 用户决策「key 不落 OCR 配置」:落盘的是命令字符串,明文只在取用时出现一次。
|
|
12
|
+
// 宿主侧写这条命令前已经用 credentials.resolve() 确认过 key 解析得到(host.ts
|
|
13
|
+
// createGateway),本脚本负责让 OCR 自己也能取到同一个值。
|
|
14
|
+
//
|
|
15
|
+
// 谁调用它:外部 ocr CLI 本身(每次取 key 起一个进程,60s 超时),命令串由
|
|
16
|
+
// lib/config-store.ts buildApiKeyCmd() 生成:`node <本文件绝对路径> <REF_NAME>`。
|
|
17
|
+
// 本包代码不会 import 它,也没有别的调用点。
|
|
18
|
+
//
|
|
19
|
+
// 数据目录怎么找(刻意不写死 ~/.dsh):显式第三参数 > $DSH_HOME > os.homedir()/.dsh
|
|
20
|
+
// —— 与宿主 util/home-paths 的 resolveDshHome 优先级一致(configured > env > 默认),
|
|
21
|
+
// 第一档在这里只能是命令行参数,因为「启动参数指定的 home」 plugin 侧也无从得知。
|
|
22
|
+
//
|
|
23
|
+
// 取值的层序镜像官方 credentials-local 的优先级里**够得到**的那两层:
|
|
24
|
+
// 1. 继承来的进程环境(dsh 的 env 层就是最高层:`DEEPSEEK_API_KEY=… dsh` 时,
|
|
25
|
+
// OCR 作为孙进程本来就继承得到,过去只读文件反而把它丢了);
|
|
26
|
+
// 2. `$DSH_HOME/.credentials.yaml` 的 `refs.<REF>`(version: 1 托管文档,
|
|
27
|
+
// 格式来源 packages/credentials/credentials-local/src/index.ts:169-216);
|
|
28
|
+
// 3. `$DSH_HOME/.env`(用户级回退层)。
|
|
29
|
+
// **项目 .env 刻意不读**:本脚本的 cwd 是 OCR 当时所在的仓库,跟 dsh 的启动工作区
|
|
30
|
+
// 不是一回事,去读它等于把「被审查仓库」里的键当成用户的凭据。
|
|
31
|
+
//
|
|
32
|
+
// OCR api_key_cmd 契约(docs/configuration):
|
|
33
|
+
// - 命令去除首尾空白后的单行 stdout 即 key;多行/空/非零退出 = 硬错误,OCR
|
|
34
|
+
// 绝不静默回退(保持硬失败语义)。
|
|
35
|
+
// - config.json 中的 api_key_cmd 属于可信输入(用户权限 0600)。
|
|
36
|
+
//
|
|
37
|
+
// 防注入:ref 名白名单校验 ^[A-Z0-9_]+$,绝不做 shell 拼接。
|
|
38
|
+
//
|
|
39
|
+
// 顶部 node reference:oxlint 类型检查对 .mjs 不自动加载 @types/node,需显式
|
|
40
|
+
// 声明才能解析 node:fs/node:os/node:process/process(否则整文件按 error 类型
|
|
41
|
+
// 误报 unsafe)。
|
|
42
|
+
|
|
43
|
+
import { readFileSync } from "node:fs";
|
|
44
|
+
import { homedir } from "node:os";
|
|
45
|
+
import path from "node:path";
|
|
46
|
+
import { load } from "js-yaml";
|
|
47
|
+
|
|
48
|
+
/** 官方 ref 语法(credentials/src/index.ts:19 REF_PATTERN)。 */
|
|
49
|
+
const REF_PATTERN = /^[A-Z0-9_]+$/u;
|
|
50
|
+
|
|
51
|
+
/** 托管凭据文档的固定文件名(credentials-local 的 CREDENTIALS_FILENAME)。 */
|
|
52
|
+
const CREDENTIALS_FILENAME = ".credentials.yaml";
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* 对象守卫(shared/lib/tool-events 同款防御):typeof 收窄为 Record 而不经断言。
|
|
56
|
+
* @param {unknown} value
|
|
57
|
+
* @returns {value is Record<string, unknown>}
|
|
58
|
+
*/
|
|
59
|
+
function isRecord(value) {
|
|
60
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* 数据目录:显式参数 > $DSH_HOME > homedir()/.dsh(空/全空白按未设置处理)。
|
|
65
|
+
* @param {string | undefined} fromArg
|
|
66
|
+
* @returns {string}
|
|
67
|
+
*/
|
|
68
|
+
function dataDirFrom(fromArg) {
|
|
69
|
+
const arg = typeof fromArg === "string" ? fromArg.trim() : "";
|
|
70
|
+
if (arg !== "") {
|
|
71
|
+
return arg;
|
|
72
|
+
}
|
|
73
|
+
const fromEnv = typeof process.env.DSH_HOME === "string" ? process.env.DSH_HOME.trim() : "";
|
|
74
|
+
return fromEnv === "" ? path.join(homedir(), ".dsh") : fromEnv;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/** 取值过程中遇到的坏消息(文件损坏/权限);只在最终失败时一并报出,
|
|
78
|
+
* 免得某一层的小毛病提前 exit 1 把后面的合法层也堵掉。 */
|
|
79
|
+
const problems = [];
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* ENOENT 判定(文件不存在是正常态,别把它当损坏报出去)。
|
|
83
|
+
* @param {unknown} error
|
|
84
|
+
* @returns {boolean}
|
|
85
|
+
*/
|
|
86
|
+
function isNotFound(error) {
|
|
87
|
+
return isRecord(error) && error.code === "ENOENT";
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* 托管 YAML 文档里的 refs.<REF>(形状不合一律回 undefined,不抛)。
|
|
92
|
+
* @param {string} credPath
|
|
93
|
+
* @param {string} refName
|
|
94
|
+
* @returns {string | undefined}
|
|
95
|
+
*/
|
|
96
|
+
function fromCredentialsDoc(credPath, refName) {
|
|
97
|
+
let text;
|
|
98
|
+
try {
|
|
99
|
+
text = readFileSync(credPath, "utf8");
|
|
100
|
+
} catch (error) {
|
|
101
|
+
// 文档不存在是正常态(只用环境/`.env` 的部署),不当错误报。
|
|
102
|
+
if (!isNotFound(error)) {
|
|
103
|
+
problems.push(`读取凭证文件失败:${credPath} ${String(error)}`);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
/** @type {unknown} */
|
|
107
|
+
let doc;
|
|
108
|
+
if (text !== undefined) {
|
|
109
|
+
try {
|
|
110
|
+
doc = load(text);
|
|
111
|
+
} catch (error) {
|
|
112
|
+
problems.push(`凭证文件不是合法 YAML:${credPath} ${String(error)}`);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
const refs = isRecord(doc) ? doc.refs : undefined;
|
|
116
|
+
const value = isRecord(refs) ? refs[refName] : undefined;
|
|
117
|
+
return typeof value === "string" && value.length > 0 ? value : undefined;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* `.env` 的单行取值:去首尾空白,长度 > 1 且以引号开头时剥掉包裹引号。
|
|
122
|
+
* @param {string} raw
|
|
123
|
+
* @returns {string}
|
|
124
|
+
*/
|
|
125
|
+
function unquotedValue(raw) {
|
|
126
|
+
const trimmed = raw.trim();
|
|
127
|
+
const quoted = trimmed.length > 1 && (trimmed.startsWith('"') || trimmed.startsWith("'"));
|
|
128
|
+
return quoted ? trimmed.slice(1, -1) : trimmed;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* $DSH_HOME/.env 的 KEY=VALUE(去掉可选引号;文件不存在按「该层没有值」处理)。
|
|
133
|
+
* @param {string} envPath
|
|
134
|
+
* @param {string} refName
|
|
135
|
+
* @returns {string | undefined}
|
|
136
|
+
*/
|
|
137
|
+
function fromUserDotEnv(envPath, refName) {
|
|
138
|
+
let text;
|
|
139
|
+
try {
|
|
140
|
+
text = readFileSync(envPath, "utf8");
|
|
141
|
+
} catch (error) {
|
|
142
|
+
if (!isNotFound(error)) {
|
|
143
|
+
problems.push(`读取 ${envPath} 失败:${String(error)}`);
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
const pattern = new RegExp(`^\\s*(?:export\\s+)?${refName}\\s*=\\s*(?<value>.*)$`, "u");
|
|
147
|
+
let hit = "";
|
|
148
|
+
for (const line of (text ?? "").split("\n")) {
|
|
149
|
+
const value = unquotedValue(pattern.exec(line)?.groups?.value ?? "");
|
|
150
|
+
if (value.length > 0) {
|
|
151
|
+
hit = value;
|
|
152
|
+
break;
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
return hit === "" ? undefined : hit;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
const refName = process.argv[2] ?? "";
|
|
159
|
+
if (!REF_PATTERN.test(refName)) {
|
|
160
|
+
process.stderr.write(`[get-cred] 非法 env key 名:${JSON.stringify(refName)}\n`);
|
|
161
|
+
process.exit(1);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
const inherited = process.env[refName];
|
|
165
|
+
const home = dataDirFrom(process.argv[3]);
|
|
166
|
+
const found =
|
|
167
|
+
(typeof inherited === "string" && inherited.trim().length > 0 ? inherited.trim() : undefined) ??
|
|
168
|
+
fromCredentialsDoc(path.join(home, CREDENTIALS_FILENAME), refName) ??
|
|
169
|
+
fromUserDotEnv(path.join(home, ".env"), refName);
|
|
170
|
+
|
|
171
|
+
if (found === undefined) {
|
|
172
|
+
// 硬失败:OCR 契约里非零退出/空输出都不回退,所以原因必须写全(含查过的目录与
|
|
173
|
+
// 各层的坏消息),否则用户只能看到「llm 请求 401」这种隔了一层的症状。
|
|
174
|
+
const notes = problems.length === 0 ? "" : `;${problems.join(";")}`;
|
|
175
|
+
process.stderr.write(
|
|
176
|
+
`[get-cred] 取不到 ${refName}:查过进程环境、${path.join(home, CREDENTIALS_FILENAME)} 与 ${path.join(home, ".env")}${notes}\n`,
|
|
177
|
+
);
|
|
178
|
+
process.exit(1);
|
|
179
|
+
} else {
|
|
180
|
+
// 成功路径与失败路径同为「一条语句」形态:exit(1) 之后的分支互斥,不落到 stdout。
|
|
181
|
+
process.stdout.write(`${found}\n`);
|
|
182
|
+
}
|