dsh-codearts 0.0.0-stage → 0.2.1

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.
@@ -0,0 +1,6 @@
1
+ # Register the CodeArts provider without changing the profile's current
2
+ # default model. The plugin uses the embedded Huawei Cloud OAuth2 login
3
+ # (PKCE + DPoP) or a pasted token; no separate CLI is required.
4
+ - insert:
5
+ - id: llm-codearts
6
+ name: dsh-codearts
package/lib/bin.d.mts ADDED
@@ -0,0 +1 @@
1
+ export {}
package/lib/bin.mjs ADDED
@@ -0,0 +1,373 @@
1
+ #!/usr/bin/env node
2
+ import { _ as CodeArtsAccountStore, c as fetchCallerIdentity, g as isBenefitModel, m as FALLBACK_CODEARTS_MODELS, n as codeartsUsagePath, r as discoverModels, t as CodeArtsUsageTracker, y as codeartsAccountsPath } from "./usage-ClRZWUS4.mjs";
3
+ import { existsSync, statSync } from "node:fs";
4
+ //#region src/version.ts
5
+ const CODEARTS_VERSION = "0.2.1";
6
+ //#endregion
7
+ //#region src/bin.ts
8
+ /**
9
+ * dsh-codearts CLI: sign-in state, diagnostics and sign-out.
10
+ *
11
+ * dsh plugin --profile web exec dsh-codearts status # sign-in state + seed models
12
+ * dsh plugin --profile web exec dsh-codearts status --json # machine-readable
13
+ * dsh plugin --profile web exec dsh-codearts models # discover the account's real models
14
+ * dsh plugin --profile web exec dsh-codearts models --json # machine-readable
15
+ * dsh plugin --profile web exec dsh-codearts usage # local token accounting
16
+ * dsh plugin --profile web exec dsh-codearts usage --json # machine-readable
17
+ * dsh plugin --profile web exec dsh-codearts identity # stable upstream user ids per account
18
+ * dsh plugin --profile web exec dsh-codearts identity --fix # merge duplicate entries onto them
19
+ * dsh plugin --profile web exec dsh-codearts doctor # secret-free diagnostics
20
+ * dsh plugin --profile web exec dsh-codearts logout # drop the plugin's credential
21
+ *
22
+ * `models` asks the upstream for the account's real catalog (built-in, limited-time
23
+ * benefit and agent-center sources) instead of printing the hand-maintained seed —
24
+ * that is the first thing to run when a model is missing from the picker.
25
+ *
26
+ * `usage` reads the plugin's own ledger of requests this machine sent. CodeArts has
27
+ * no quota API, so it is not an account balance.
28
+ *
29
+ * `identity` asks the gateway which account each stored credential belongs to. The
30
+ * login response carries no user id, so credentials used to be keyed by their DPoP
31
+ * key — which made a re-login of the same account look like a new one. This is the
32
+ * command that reports and (with --fix) merges those duplicates.
33
+ *
34
+ * `doctor` never prints token material — only paths, booleans and versions —
35
+ * so its output is safe to paste into a bug report. `logout` removes only this
36
+ * plugin's own credential copy; nothing outside the plugin is touched.
37
+ *
38
+ * @module dsh-codearts/bin
39
+ */
40
+ /** Bumped when a `--json` document changes shape (5: identity backfill). */
41
+ const JSON_SCHEMA_VERSION = 5;
42
+ /**
43
+ * 一句话描述一个账号,供人类可读输出用。
44
+ *
45
+ * 注意空串也要当「没有」:`credentialFromOAuth` 会把上游返回的空 `user_name` /
46
+ * `user_id` 原样写进 label,用 `??` 兜底会得到一个空名字,于是 `status` 打印出
47
+ * `- (expires …)` 这种认不出是哪个账号的行。
48
+ */
49
+ function describeAccount(account) {
50
+ const name = firstNonEmpty(account.label, account.userName, `${account.id.slice(0, 12)}…`);
51
+ const notes = [];
52
+ if (account.stale) notes.push("needs sign-in");
53
+ else if (typeof account.expiration === "string") notes.push(`expires ${account.expiration}`);
54
+ if (!account.signable) notes.push("no AK/SK");
55
+ return notes.length === 0 ? name : `${name} (${notes.join(", ")})`;
56
+ }
57
+ /** 第一个非空(去空白后)的字符串。 */
58
+ function firstNonEmpty(...values) {
59
+ for (const value of values) if (typeof value === "string" && value.trim() !== "") return value.trim();
60
+ return "";
61
+ }
62
+ function store() {
63
+ return new CodeArtsAccountStore(codeartsAccountsPath());
64
+ }
65
+ async function status(jsonOutput) {
66
+ const accounts = await store().list();
67
+ const cred = await store().current();
68
+ const out = {
69
+ schemaVersion: JSON_SCHEMA_VERSION,
70
+ version: CODEARTS_VERSION,
71
+ signedIn: accounts.length > 0,
72
+ signable: accounts.some((account) => account.signable),
73
+ accounts,
74
+ models: FALLBACK_CODEARTS_MODELS.map((m) => ({
75
+ id: m.id,
76
+ name: m.name
77
+ }))
78
+ };
79
+ if (cred !== void 0) {
80
+ if (cred.label !== void 0) out.label = cred.label;
81
+ if (cred.storedAt !== void 0) out.storedAt = cred.storedAt;
82
+ if (cred.expiration !== void 0) out.expiration = cred.expiration;
83
+ if (cred.stale === true) out.stale = true;
84
+ }
85
+ if (jsonOutput) {
86
+ process.stdout.write(JSON.stringify(out, null, 2) + "\n");
87
+ return out.signedIn ? 0 : 1;
88
+ }
89
+ if (out.signedIn) {
90
+ process.stdout.write(`signed in — ${accounts.length} account${accounts.length === 1 ? "" : "s"}:\n`);
91
+ for (const account of accounts) process.stdout.write(` - ${describeAccount(account)}\n`);
92
+ if (!out.signable) process.stdout.write("warning: no account carries AK/SK, so requests cannot be signed and chats will be rejected\n");
93
+ } else process.stdout.write("not signed in (no account stored; sign in from the CodeArts settings card)\n");
94
+ process.stdout.write(`models (${out.models.length}):\n`);
95
+ for (const m of out.models) process.stdout.write(` - ${m.id} ${m.name}\n`);
96
+ return out.signedIn ? 0 : 1;
97
+ }
98
+ /**
99
+ * 动态发现当前可用模型(三路:内置 / 限时福利 / agent-center)。
100
+ *
101
+ * 这是排查「模型选择器里没有某个模型」的第一手段:它打印的是上游真实返回,
102
+ * 而不是插件里那份手维护的种子清单。未登录时退回种子清单并说明原因。
103
+ */
104
+ async function models(jsonOutput) {
105
+ if ((await store().list()).length === 0) {
106
+ if (jsonOutput) process.stdout.write(JSON.stringify({
107
+ schemaVersion: JSON_SCHEMA_VERSION,
108
+ version: CODEARTS_VERSION,
109
+ discovered: false,
110
+ reason: "not signed in",
111
+ models: FALLBACK_CODEARTS_MODELS.map((m) => ({
112
+ id: m.id,
113
+ name: m.name,
114
+ benefit: isBenefitModel(m.id)
115
+ }))
116
+ }, null, 2) + "\n");
117
+ else {
118
+ process.stdout.write("not signed in — showing the built-in seed catalog\n");
119
+ for (const m of FALLBACK_CODEARTS_MODELS) process.stdout.write(` - ${m.id} ${m.name}${isBenefitModel(m.id) ? " [benefit]" : ""}\n`);
120
+ }
121
+ return 1;
122
+ }
123
+ const outcome = await discoverModels(store());
124
+ if (jsonOutput) {
125
+ process.stdout.write(JSON.stringify({
126
+ schemaVersion: JSON_SCHEMA_VERSION,
127
+ version: CODEARTS_VERSION,
128
+ discovered: outcome.anySucceeded,
129
+ accounts: outcome.accounts.map((account) => ({
130
+ accountId: account.accountId,
131
+ label: account.label,
132
+ ok: account.ok,
133
+ sources: account.sources,
134
+ modelCount: account.models.length,
135
+ errors: account.errors
136
+ })),
137
+ models: outcome.models.map((m) => ({
138
+ id: m.id,
139
+ name: m.name,
140
+ contextWindow: m.contextWindow,
141
+ maxTokens: m.maxTokens,
142
+ supportsImages: m.supportsImages,
143
+ benefit: m.benefit === true || isBenefitModel(m.id)
144
+ }))
145
+ }, null, 2) + "\n");
146
+ return outcome.anySucceeded ? 0 : 1;
147
+ }
148
+ for (const account of outcome.accounts) {
149
+ const name = account.label ?? account.accountId;
150
+ process.stdout.write(account.ok ? `discovered ${account.models.length} model(s) for ${name} via ${account.sources.join(", ")}\n` : `discovery failed for ${name}: ${account.errors.map((e) => `${e.source}: ${e.message}`).join("; ")}\n`);
151
+ for (const error of account.ok ? account.errors : []) process.stdout.write(` warning: ${error.source} source failed (${error.message})\n`);
152
+ }
153
+ if (!outcome.anySucceeded) {
154
+ process.stdout.write("no account could be queried; nothing to list\n");
155
+ return 1;
156
+ }
157
+ process.stdout.write(`models (${outcome.models.length}):\n`);
158
+ for (const m of outcome.models) {
159
+ const flags = [
160
+ m.benefit === true || isBenefitModel(m.id) ? "benefit" : "",
161
+ m.supportsImages ? "images" : "",
162
+ `max ${m.maxTokens}`,
163
+ `ctx ${m.contextWindow}`
164
+ ].filter(Boolean).join(", ");
165
+ process.stdout.write(` - ${m.id} ${m.name} (${flags})\n`);
166
+ }
167
+ return 0;
168
+ }
169
+ /**
170
+ * 本机用量账本(插件自己记的,不是账号额度)。
171
+ *
172
+ * 为什么要用 CLI 暴露它:账本是插件进程边跑边写的,GUI 卡片能看,但这个命令让
173
+ * 「今天到底花了多少 token」在无图形界面时也能查,且输出可以直接贴进问题反馈。
174
+ */
175
+ async function usage(jsonOutput) {
176
+ const tracker = new CodeArtsUsageTracker({ path: codeartsUsagePath() });
177
+ await tracker.load();
178
+ const snapshot = tracker.snapshot();
179
+ if (jsonOutput) {
180
+ process.stdout.write(JSON.stringify({
181
+ schemaVersion: JSON_SCHEMA_VERSION,
182
+ version: CODEARTS_VERSION,
183
+ today: snapshot.today,
184
+ allTime: snapshot.allTime,
185
+ byModelToday: snapshot.byModelToday,
186
+ byAccountToday: snapshot.byAccountToday,
187
+ days: snapshot.days,
188
+ lastAt: snapshot.lastAt
189
+ }, null, 2) + "\n");
190
+ return 0;
191
+ }
192
+ if (snapshot.allTime.requests === 0) {
193
+ process.stdout.write("no usage recorded yet (nothing this plugin has sent)\n");
194
+ return 0;
195
+ }
196
+ process.stdout.write(`today ${snapshot.today.requests} request(s), in ${snapshot.today.promptTokens} / out ${snapshot.today.completionTokens} tokens\n`);
197
+ process.stdout.write(`all time ${snapshot.allTime.requests} request(s), in ${snapshot.allTime.promptTokens} / out ${snapshot.allTime.completionTokens} tokens (${snapshot.days} day(s) kept)\n`);
198
+ if (snapshot.byModelToday.length > 0) {
199
+ process.stdout.write("today by model:\n");
200
+ for (const entry of snapshot.byModelToday) process.stdout.write(` - ${entry.id} ${entry.totals.requests} req in ${entry.totals.promptTokens} / out ${entry.totals.completionTokens}\n`);
201
+ }
202
+ const accounts = Object.entries(snapshot.byAccountToday);
203
+ if (accounts.length > 0) {
204
+ process.stdout.write("today by account:\n");
205
+ for (const [id, totals] of accounts) process.stdout.write(` - ${id.slice(0, 12)}… ${totals.requests} req in ${totals.promptTokens} / out ${totals.completionTokens}\n`);
206
+ }
207
+ process.stdout.write("note: CodeArts exposes no quota API; this counts requests this machine sent through the plugin\n");
208
+ return 0;
209
+ }
210
+ /**
211
+ * 身份回填与重复账号清理。
212
+ *
213
+ * 背景:上游登录响应里的 `user_id` 是空串,插件原先只能拿 DPoP 公钥当账号键,
214
+ * 于是「对同一个账号重新登录」会多出一条记录(旧的那条之后因凭据失效而停用)。
215
+ * `/snap-manager/v1/current/user` 能给出稳定 user_id,所以:
216
+ * - 新登录会自动带上身份(见 web-status 的登录流程);
217
+ * - 历史遗留的条目用 `identity --fix` 合并到稳定键上。
218
+ */
219
+ async function identity(jsonOutput, fix) {
220
+ const accounts = await store();
221
+ const ids = await accounts.ids();
222
+ if (ids.length === 0) {
223
+ if (jsonOutput) process.stdout.write(JSON.stringify({
224
+ schemaVersion: JSON_SCHEMA_VERSION,
225
+ fixed: fix,
226
+ duplicateIdentities: 0,
227
+ accounts: []
228
+ }, null, 2) + "\n");
229
+ else process.stdout.write("not signed in — nothing to inspect\n");
230
+ return 1;
231
+ }
232
+ const rows = [];
233
+ for (const id of ids) {
234
+ const resolved = await accounts.resolve(id);
235
+ if (resolved === void 0) {
236
+ rows.push({
237
+ id,
238
+ ok: false,
239
+ error: "credential unavailable (sign in again)"
240
+ });
241
+ continue;
242
+ }
243
+ try {
244
+ const found = await fetchCallerIdentity(resolved);
245
+ const action = fix ? await accounts.adoptIdentity(id, found) : void 0;
246
+ rows.push({
247
+ id,
248
+ userId: found.userId,
249
+ ...found.userName === "" ? {} : { userName: found.userName },
250
+ ok: true,
251
+ ...action === void 0 ? {} : { action }
252
+ });
253
+ } catch (error) {
254
+ rows.push({
255
+ id,
256
+ ok: false,
257
+ error: error instanceof Error ? error.message : String(error)
258
+ });
259
+ }
260
+ }
261
+ const byUser = /* @__PURE__ */ new Map();
262
+ for (const row of rows) if (row.userId !== void 0) byUser.set(row.userId, (byUser.get(row.userId) ?? 0) + 1);
263
+ const duplicates = [...byUser.entries()].filter(([, count]) => count > 1).length;
264
+ if (jsonOutput) {
265
+ process.stdout.write(JSON.stringify({
266
+ schemaVersion: JSON_SCHEMA_VERSION,
267
+ fixed: fix,
268
+ duplicateIdentities: duplicates,
269
+ accounts: rows
270
+ }, null, 2) + "\n");
271
+ return 0;
272
+ }
273
+ for (const row of rows) {
274
+ if (!row.ok) {
275
+ process.stdout.write(` - ${row.id.slice(0, 12)}… identity probe failed: ${row.error}\n`);
276
+ continue;
277
+ }
278
+ const label = row.userName === void 0 ? "" : ` (${row.userName})`;
279
+ const acted = row.action === void 0 ? "" : ` [${row.action}]`;
280
+ process.stdout.write(` - ${row.id.slice(0, 12)}… -> ${row.userId}${label}${acted}\n`);
281
+ }
282
+ if (fix) process.stdout.write("identity backfilled; duplicate entries were merged into the stable user id\n");
283
+ else if (duplicates > 0) process.stdout.write(`found ${duplicates} user id(s) stored more than once; re-run with --fix to merge them\n`);
284
+ else process.stdout.write("no duplicate accounts found\n");
285
+ return 0;
286
+ }
287
+ async function doctor(jsonOutput) {
288
+ const accountsPath = codeartsAccountsPath();
289
+ const exists = existsSync(accountsPath);
290
+ const accounts = await store().list();
291
+ const signable = accounts.some((account) => account.signable);
292
+ const hints = [];
293
+ if (accounts.length === 0) hints.push("No account stored yet: sign in from the CodeArts settings card.");
294
+ else {
295
+ const stale = accounts.filter((account) => account.stale);
296
+ if (stale.length > 0) hints.push(`${stale.length} of ${accounts.length} account(s) were marked stale after a failed refresh; sign in again.`);
297
+ if (!signable) hints.push("No account carries AK/SK. CodeArts rejects unsigned chat requests — sign in with the OAuth flow rather than pasting a bare token.");
298
+ }
299
+ if (!process.env.DSH_HOME) hints.push("DSH_HOME is unset; the credential path fell back to ~/.dsh — set DSH_HOME if your harness lives elsewhere.");
300
+ const out = {
301
+ schemaVersion: JSON_SCHEMA_VERSION,
302
+ version: CODEARTS_VERSION,
303
+ node: process.version,
304
+ platform: `${process.platform} ${process.arch}`,
305
+ authPath: accountsPath,
306
+ authFileExists: exists,
307
+ signedIn: accounts.length > 0,
308
+ signable,
309
+ accounts,
310
+ hints
311
+ };
312
+ if (jsonOutput) {
313
+ process.stdout.write(JSON.stringify(out, null, 2) + "\n");
314
+ return hints.length === 0 ? 0 : 1;
315
+ }
316
+ process.stdout.write(`dsh-codearts ${out.version} (node ${out.node}, ${out.platform})\n`);
317
+ process.stdout.write(`accounts: ${out.authPath}\n`);
318
+ process.stdout.write(` file exists : ${out.authFileExists ? "yes" : "no"}${exists ? ` (${statSync(accountsPath).size} bytes)` : ""}\n`);
319
+ process.stdout.write(` accounts : ${accounts.length}\n`);
320
+ for (const account of accounts) process.stdout.write(` - ${describeAccount(account)}\n`);
321
+ process.stdout.write(` signable : ${out.signable ? "yes (AK/SK present)" : "no (no AK/SK)"}\n`);
322
+ if (hints.length === 0) process.stdout.write("no issues found\n");
323
+ else {
324
+ process.stdout.write("hints:\n");
325
+ for (const hint of hints) process.stdout.write(` - ${hint}\n`);
326
+ }
327
+ return hints.length === 0 ? 0 : 1;
328
+ }
329
+ async function logout(jsonOutput) {
330
+ const accountsPath = codeartsAccountsPath();
331
+ const removed = (await store().list()).length;
332
+ await store().clear();
333
+ if (jsonOutput) process.stdout.write(JSON.stringify({
334
+ schemaVersion: JSON_SCHEMA_VERSION,
335
+ signedOut: true,
336
+ removed,
337
+ authPath: accountsPath
338
+ }, null, 2) + "\n");
339
+ else process.stdout.write(`signed out; removed ${removed} account${removed === 1 ? "" : "s"}\n`);
340
+ return 0;
341
+ }
342
+ async function main() {
343
+ const args = process.argv.slice(2);
344
+ const jsonOutput = args.includes("--json");
345
+ const fix = args.includes("--fix");
346
+ const positional = args.filter((arg) => !arg.startsWith("--"));
347
+ const unknown = args.filter((arg) => arg.startsWith("--") && arg !== "--json" && arg !== "--fix");
348
+ const raw = positional[0] ?? "status";
349
+ if (![
350
+ "doctor",
351
+ "identity",
352
+ "logout",
353
+ "models",
354
+ "status",
355
+ "usage"
356
+ ].includes(raw)) {
357
+ process.stderr.write(`dsh-codearts: expected doctor, identity, logout, models, status, or usage; got ${JSON.stringify(raw)}\n`);
358
+ process.exit(2);
359
+ }
360
+ if (unknown.length > 0) {
361
+ process.stderr.write(`dsh-codearts: unknown flag ${JSON.stringify(unknown[0])} (only --json and --fix are supported)\n`);
362
+ process.exit(2);
363
+ }
364
+ const action = raw;
365
+ const code = action === "doctor" ? await doctor(jsonOutput) : action === "identity" ? await identity(jsonOutput, fix) : action === "logout" ? await logout(jsonOutput) : action === "models" ? await models(jsonOutput) : action === "usage" ? await usage(jsonOutput) : await status(jsonOutput);
366
+ process.exit(code);
367
+ }
368
+ main().catch((error) => {
369
+ process.stderr.write(`${error instanceof Error ? error.message : String(error)}\n`);
370
+ process.exit(1);
371
+ });
372
+ //#endregion
373
+ export {};
@@ -0,0 +1,86 @@
1
+ import { Context } from "@deepseek-ai/cordis";
2
+ //#region src/client/locales.d.ts
3
+ interface CodeArtsSettingsKey {
4
+ tabLabel: string;
5
+ cardTitle: string;
6
+ statusSignedIn: string;
7
+ statusSignedOut: string;
8
+ statusExpired: string;
9
+ descSignedIn: string;
10
+ descSignedOut: string;
11
+ storedMeta: string;
12
+ storedMetaNoLabel: string;
13
+ refresh: string;
14
+ clearToken: string;
15
+ tokenLabel: string;
16
+ tokenPlaceholder: string;
17
+ labelLabel: string;
18
+ labelPlaceholder: string;
19
+ saveToken: string;
20
+ expiredHint: string;
21
+ accountsTitle: string;
22
+ accountRemove: string;
23
+ addAccount: string;
24
+ accountsRotateHint: string;
25
+ accountNeedsSignIn: string;
26
+ accountNoKey: string;
27
+ accountStored: string;
28
+ accountExpires: string;
29
+ accountExpired: string;
30
+ modelsTitle: string;
31
+ optionalModelsHint: string;
32
+ optionalModelsAllHint: string;
33
+ selectedCount: string;
34
+ selectAll: string;
35
+ free: string;
36
+ saveModels: string;
37
+ saving: string;
38
+ saved: string;
39
+ signIn: string;
40
+ signingIn: string;
41
+ openManually: string;
42
+ manualToken: string;
43
+ loginFailed: string;
44
+ modelsLoading: string;
45
+ usageTitle: string;
46
+ usageToday: string;
47
+ usageRequests: string;
48
+ usageTokens: string;
49
+ usageAllTime: string;
50
+ usageByModel: string;
51
+ usageSlots: string;
52
+ usageStateOk: string;
53
+ usageStateCapped: string;
54
+ usageStateCooling: string;
55
+ usageStateDisabled: string;
56
+ usageNoQuota: string;
57
+ usageEmpty: string;
58
+ [key: string]: string;
59
+ }
60
+ //#endregion
61
+ //#region src/client/index.d.ts
62
+ declare module '@deepseek-ai/dsh-client-ui-slots' {
63
+ interface LocaleNamespaceMap {
64
+ 'settings.codearts': CodeArtsSettingsKey;
65
+ }
66
+ /**
67
+ * rc.2 宿主把插件配置页挂在 plugins.bundle.config(按 bundle 包名取 key)。
68
+ * 这个槽位只在宿主运行时声明 —— 连 0.2.0-rc.2 的 settings-plugins 类型里都没有
69
+ * (它导出的槽位是 settings.plugins.tab / settings.general.item 那一组),
70
+ * 所以只能我们自己补,让类型跟运行时的真槽位对上。
71
+ */
72
+ interface SlotMap {
73
+ 'plugins.bundle.config': {
74
+ kind: 'keyed';
75
+ scope: 'root';
76
+ owner: {
77
+ children?: never;
78
+ };
79
+ };
80
+ }
81
+ }
82
+ declare const name = "dsh-codearts-client";
83
+ declare const inject: string[];
84
+ declare function apply(ctx: Context): void;
85
+ //#endregion
86
+ export { apply, inject, name };