dsh-dock 0.7.0 → 0.7.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.
- package/README.md +3 -13
- package/client.js +746 -2750
- package/features/animation/view.jsx +491 -18
- package/features/balance/view.js +25 -4
- package/features/games/games/breakout.jsx +225 -0
- package/features/games/games/gomoku.jsx +175 -0
- package/features/games/games/racer.jsx +171 -0
- package/features/games/games/snake.jsx +103 -0
- package/features/games/games/sokoban.jsx +153 -0
- package/features/games/games/tank.jsx +116 -0
- package/features/games/games/tetris.jsx +235 -0
- package/features/games/games/xiangqi.jsx +128 -0
- package/features/games/tank-core.js +152 -0
- package/features/games/view.jsx +632 -0
- package/features/games/xiangqi-core.js +270 -0
- package/features/tokenlog/host.js +2 -2
- package/features/tokenlog/view.jsx +12 -21
- package/index.js +0 -2
- package/package.json +1 -1
- package/src/client.jsx +40 -24
- package/src/host-core.js +4 -1
|
@@ -0,0 +1,270 @@
|
|
|
1
|
+
/* 趣味游戏 · 象棋核心规则(纯函数,无 React)——供组件复用,也便于单测。 */
|
|
2
|
+
export const ROWS = 10, COLS = 9;
|
|
3
|
+
// 类型:g 将/帅 · a 仕/士 · e 相/象 · h 马 · k 车 · c 炮 · p 兵/卒
|
|
4
|
+
// 颜色:r 红(下)· b 黑(上)
|
|
5
|
+
|
|
6
|
+
export function pieceChar(p) {
|
|
7
|
+
const map = {
|
|
8
|
+
g: { r: "帥", b: "將" }, a: { r: "仕", b: "士" }, e: { r: "相", b: "象" },
|
|
9
|
+
h: { r: "馬", b: "馬" }, k: { r: "車", b: "車" }, c: { r: "炮", b: "炮" },
|
|
10
|
+
p: { r: "兵", b: "卒" },
|
|
11
|
+
};
|
|
12
|
+
return map[p.t] && map[p.t][p.c];
|
|
13
|
+
}
|
|
14
|
+
export const VALUE = { g: 100000, k: 900, c: 450, h: 400, e: 200, a: 200, p: 100 };
|
|
15
|
+
|
|
16
|
+
export function initialBoard() {
|
|
17
|
+
const b = Array.from({ length: ROWS }, () => Array(COLS).fill(null));
|
|
18
|
+
const back = ["k", "h", "e", "a", "g", "a", "e", "h", "k"];
|
|
19
|
+
back.forEach((t, c) => { b[0][c] = { t, c: "b" }; });
|
|
20
|
+
b[2][1] = { t: "c", c: "b" }; b[2][7] = { t: "c", c: "b" };
|
|
21
|
+
[0, 2, 4, 6, 8].forEach((c) => { b[4][c] = { t: "p", c: "b" }; });
|
|
22
|
+
back.forEach((t, c) => { b[9][c] = { t, c: "r" }; });
|
|
23
|
+
b[8][1] = { t: "c", c: "r" }; b[8][7] = { t: "c", c: "r" };
|
|
24
|
+
[0, 2, 4, 6, 8].forEach((c) => { b[6][c] = { t: "p", c: "r" }; });
|
|
25
|
+
return b;
|
|
26
|
+
}
|
|
27
|
+
export function cloneBoard(b) {
|
|
28
|
+
return b.map((row) => row.slice());
|
|
29
|
+
}
|
|
30
|
+
function inBounds(r, c) { return r >= 0 && r < ROWS && c >= 0 && c < COLS; }
|
|
31
|
+
function inPalace(r, c, color) {
|
|
32
|
+
if (c < 3 || c > 5) return false;
|
|
33
|
+
return color === "r" ? r >= 7 && r <= 9 : r >= 0 && r <= 2;
|
|
34
|
+
}
|
|
35
|
+
function ownSide(r, color) { return color === "r" ? r >= 5 : r <= 4; }
|
|
36
|
+
function crossedRiver(r, color) { return color === "r" ? r <= 4 : r >= 5; }
|
|
37
|
+
function countBetween(board, r, c, tr, tc, dr, dc) {
|
|
38
|
+
let n = 0, rr = r + dr, cc = c + dc;
|
|
39
|
+
while (!(rr === tr && cc === tc)) {
|
|
40
|
+
if (rr < 0 || rr >= ROWS || cc < 0 || cc >= COLS) return -1;
|
|
41
|
+
if (board[rr][cc]) n += 1;
|
|
42
|
+
rr += dr; cc += dc;
|
|
43
|
+
}
|
|
44
|
+
return n;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function attacks(board, r, c, tr, tc) {
|
|
48
|
+
const p = board[r][c];
|
|
49
|
+
if (!p) return false;
|
|
50
|
+
const dr = Math.sign(tr - r), dc = Math.sign(tc - c);
|
|
51
|
+
const t = board[tr][tc];
|
|
52
|
+
if (p.t === "k") {
|
|
53
|
+
if (r !== tr && c !== tc) return false;
|
|
54
|
+
return countBetween(board, r, c, tr, tc, dr, dc) === 0;
|
|
55
|
+
}
|
|
56
|
+
if (p.t === "c") {
|
|
57
|
+
if (r !== tr && c !== tc) return false;
|
|
58
|
+
if (dr === 0 && dc === 0) return false;
|
|
59
|
+
return countBetween(board, r, c, tr, tc, dr, dc) === 1 && !!t;
|
|
60
|
+
}
|
|
61
|
+
if (p.t === "h") {
|
|
62
|
+
const adr = Math.abs(tr - r), adc = Math.abs(tc - c);
|
|
63
|
+
if (!((adr === 2 && adc === 1) || (adr === 1 && adc === 2))) return false;
|
|
64
|
+
const legR = adr === 2 ? r + dr : r;
|
|
65
|
+
const legC = adc === 2 ? c + dc : c;
|
|
66
|
+
return !board[legR][legC];
|
|
67
|
+
}
|
|
68
|
+
if (p.t === "g") {
|
|
69
|
+
return Math.abs(tr - r) + Math.abs(tc - c) === 1 && inPalace(tr, tc, p.c);
|
|
70
|
+
}
|
|
71
|
+
if (p.t === "a") {
|
|
72
|
+
return Math.abs(tr - r) === 1 && Math.abs(tc - c) === 1 && inPalace(tr, tc, p.c);
|
|
73
|
+
}
|
|
74
|
+
if (p.t === "e") {
|
|
75
|
+
const adr = Math.abs(tr - r), adc = Math.abs(tc - c);
|
|
76
|
+
if (!(adr === 2 && adc === 2)) return false;
|
|
77
|
+
if (!ownSide(tr, p.c)) return false;
|
|
78
|
+
const eyeR = r + dr, eyeC = c + dc;
|
|
79
|
+
return !board[eyeR][eyeC];
|
|
80
|
+
}
|
|
81
|
+
if (p.t === "p") {
|
|
82
|
+
const forward = p.c === "r" ? r - 1 : r + 1;
|
|
83
|
+
if (tr === forward && tc === c) return true;
|
|
84
|
+
if (crossedRiver(r, p.c) && tr === r && Math.abs(tc - c) === 1) return true;
|
|
85
|
+
return false;
|
|
86
|
+
}
|
|
87
|
+
return false;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export function findGeneral(board, color) {
|
|
91
|
+
for (let r = 0; r < ROWS; r += 1) {
|
|
92
|
+
for (let c = 0; c < COLS; c += 1) {
|
|
93
|
+
const p = board[r][c];
|
|
94
|
+
if (p && p.t === "g" && p.c === color) return { r, c };
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
return null;
|
|
98
|
+
}
|
|
99
|
+
export function pseudoMoves(board, r, c) {
|
|
100
|
+
const p = board[r][c];
|
|
101
|
+
if (!p) return [];
|
|
102
|
+
const out = [];
|
|
103
|
+
const push = (nr, nc) => { if (inBounds(nr, nc)) out.push([nr, nc]); };
|
|
104
|
+
const clear = (nr, nc) => !board[nr][nc];
|
|
105
|
+
const enemy = (nr, nc) => { const q = board[nr][nc]; return q && q.c !== p.c; };
|
|
106
|
+
if (p.t === "k") {
|
|
107
|
+
[[-1, 0], [1, 0], [0, -1], [0, 1]].forEach(([dr, dc]) => {
|
|
108
|
+
const nr = r + dr, nc = c + dc;
|
|
109
|
+
if (inPalace(nr, nc, p.c) && (clear(nr, nc) || enemy(nr, nc))) push(nr, nc);
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
if (p.t === "a") {
|
|
113
|
+
[[-1, -1], [-1, 1], [1, -1], [1, 1]].forEach(([dr, dc]) => {
|
|
114
|
+
const nr = r + dr, nc = c + dc;
|
|
115
|
+
if (inPalace(nr, nc, p.c) && (clear(nr, nc) || enemy(nr, nc))) push(nr, nc);
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
if (p.t === "e") {
|
|
119
|
+
[[-2, -2], [-2, 2], [2, -2], [2, 2]].forEach(([dr, dc]) => {
|
|
120
|
+
const nr = r + dr, nc = c + dc, er = r + dr / 2, ec = c + dc / 2;
|
|
121
|
+
if (inBounds(nr, nc) && ownSide(nr, p.c) && !board[er][ec] && (clear(nr, nc) || enemy(nr, nc))) push(nr, nc);
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
if (p.t === "h") {
|
|
125
|
+
[[-2, -1], [-2, 1], [2, -1], [2, 1], [-1, -2], [-1, 2], [1, -2], [1, 2]].forEach(([dr, dc]) => {
|
|
126
|
+
const nr = r + dr, nc = c + dc;
|
|
127
|
+
const legR = Math.abs(dr) === 2 ? r + dr / 2 : r;
|
|
128
|
+
const legC = Math.abs(dc) === 2 ? c + dc / 2 : c;
|
|
129
|
+
if (inBounds(nr, nc) && !board[legR][legC] && (clear(nr, nc) || enemy(nr, nc))) push(nr, nc);
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
if (p.t === "k" || p.t === "c") {
|
|
133
|
+
[[-1, 0], [1, 0], [0, -1], [0, 1]].forEach(([dr, dc]) => {
|
|
134
|
+
let rr = r + dr, cc = c + dc, jumped = false;
|
|
135
|
+
while (inBounds(rr, cc)) {
|
|
136
|
+
const q = board[rr][cc];
|
|
137
|
+
if (p.t === "k") {
|
|
138
|
+
if (!q) push(rr, cc);
|
|
139
|
+
else { if (q.c !== p.c) push(rr, cc); break; }
|
|
140
|
+
} else {
|
|
141
|
+
if (!q && !jumped) push(rr, cc);
|
|
142
|
+
else if (q && !jumped) jumped = true;
|
|
143
|
+
else if (q && jumped) { if (q.c !== p.c) push(rr, cc); break; }
|
|
144
|
+
}
|
|
145
|
+
rr += dr; cc += dc;
|
|
146
|
+
}
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
if (p.t === "p") {
|
|
150
|
+
const forward = p.c === "r" ? -1 : 1;
|
|
151
|
+
[[forward, 0], [0, -1], [0, 1]].forEach(([dr, dc]) => {
|
|
152
|
+
const nr = r + dr, nc = c + dc;
|
|
153
|
+
if (!inBounds(nr, nc) || (dc !== 0 && !crossedRiver(r, p.c))) return;
|
|
154
|
+
if (clear(nr, nc) || enemy(nr, nc)) push(nr, nc);
|
|
155
|
+
});
|
|
156
|
+
}
|
|
157
|
+
return out;
|
|
158
|
+
}
|
|
159
|
+
export function applyMove(board, fr, fc, tr, tc) {
|
|
160
|
+
const nb = cloneBoard(board);
|
|
161
|
+
nb[tr][tc] = nb[fr][fc];
|
|
162
|
+
nb[fr][fc] = null;
|
|
163
|
+
return nb;
|
|
164
|
+
}
|
|
165
|
+
export function inCheck(board, color) {
|
|
166
|
+
const g = findGeneral(board, color);
|
|
167
|
+
if (!g) return true;
|
|
168
|
+
const enemy = color === "r" ? "b" : "r";
|
|
169
|
+
for (let r = 0; r < ROWS; r += 1) {
|
|
170
|
+
const p = board[r][g.c];
|
|
171
|
+
if (p && p.t === "g" && p.c === enemy) {
|
|
172
|
+
let clear = true;
|
|
173
|
+
const lo = Math.min(r, g.r), hi = Math.max(r, g.r);
|
|
174
|
+
for (let i = lo + 1; i < hi; i += 1) if (board[i][g.c]) { clear = false; break; }
|
|
175
|
+
if (clear) return true;
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
for (let r = 0; r < ROWS; r += 1) {
|
|
179
|
+
for (let c = 0; c < COLS; c += 1) {
|
|
180
|
+
const p = board[r][c];
|
|
181
|
+
if (p && p.c === enemy && attacks(board, r, c, g.r, g.c)) return true;
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
return false;
|
|
185
|
+
}
|
|
186
|
+
export function legalMoves(board, color) {
|
|
187
|
+
const out = [];
|
|
188
|
+
for (let r = 0; r < ROWS; r += 1) {
|
|
189
|
+
for (let c = 0; c < COLS; c += 1) {
|
|
190
|
+
const p = board[r][c];
|
|
191
|
+
if (!p || p.c !== color) continue;
|
|
192
|
+
for (const [nr, nc] of pseudoMoves(board, r, c)) {
|
|
193
|
+
const nb = applyMove(board, r, c, nr, nc);
|
|
194
|
+
if (!inCheck(nb, color)) out.push([r, c, nr, nc]);
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
return out;
|
|
199
|
+
}
|
|
200
|
+
export function hasLegalMoves(board, color) {
|
|
201
|
+
return legalMoves(board, color).length > 0;
|
|
202
|
+
}
|
|
203
|
+
function evaluatePosition(board) {
|
|
204
|
+
let score = 0;
|
|
205
|
+
for (let r = 0; r < ROWS; r += 1) {
|
|
206
|
+
for (let c = 0; c < COLS; c += 1) {
|
|
207
|
+
const p = board[r][c];
|
|
208
|
+
if (!p) continue;
|
|
209
|
+
let v = VALUE[p.t];
|
|
210
|
+
if (p.t === "p") v += crossedRiver(r, p.c) ? 50 : 0;
|
|
211
|
+
score += p.c === "b" ? v : -v;
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
return score;
|
|
215
|
+
}
|
|
216
|
+
function evalFrom(board, color) {
|
|
217
|
+
const e = evaluatePosition(board);
|
|
218
|
+
return color === "b" ? e : -e;
|
|
219
|
+
}
|
|
220
|
+
function negamax(board, color, depth, alpha, beta) {
|
|
221
|
+
const moves = legalMoves(board, color);
|
|
222
|
+
if (moves.length === 0) {
|
|
223
|
+
return color === "b" ? -1000000 + (10 - depth) : 1000000 - (10 - depth);
|
|
224
|
+
}
|
|
225
|
+
if (depth === 0) return evalFrom(board, color);
|
|
226
|
+
const enemy = color === "r" ? "b" : "r";
|
|
227
|
+
const ordered = orderMoves(board, moves);
|
|
228
|
+
let best = -Infinity;
|
|
229
|
+
for (const m of ordered) {
|
|
230
|
+
const nb = applyMove(board, m[0], m[1], m[2], m[3]);
|
|
231
|
+
const score = -negamax(nb, enemy, depth - 1, -beta, -alpha);
|
|
232
|
+
if (score > best) best = score;
|
|
233
|
+
if (best > alpha) alpha = best;
|
|
234
|
+
if (alpha >= beta) break;
|
|
235
|
+
}
|
|
236
|
+
return best;
|
|
237
|
+
}
|
|
238
|
+
function orderMoves(board, moves) {
|
|
239
|
+
const scored = moves.map((m) => {
|
|
240
|
+
let s = 0;
|
|
241
|
+
const target = board[m[2]][m[3]];
|
|
242
|
+
if (target) s += VALUE[target.t] * 10;
|
|
243
|
+
return { m, s };
|
|
244
|
+
});
|
|
245
|
+
scored.sort((a, b) => b.s - a.s);
|
|
246
|
+
return scored.map((o) => o.m);
|
|
247
|
+
}
|
|
248
|
+
export function bestMove(board, color, depth) {
|
|
249
|
+
const moves = legalMoves(board, color);
|
|
250
|
+
if (!moves.length) return null;
|
|
251
|
+
if (depth <= 1) {
|
|
252
|
+
let best = moves[0], bestScore = -Infinity;
|
|
253
|
+
for (const m of moves) {
|
|
254
|
+
const nb = applyMove(board, m[0], m[1], m[2], m[3]);
|
|
255
|
+
const s = evalFrom(nb, color);
|
|
256
|
+
if (s > bestScore) { bestScore = s; best = m; }
|
|
257
|
+
}
|
|
258
|
+
return best;
|
|
259
|
+
}
|
|
260
|
+
const enemy = color === "r" ? "b" : "r";
|
|
261
|
+
const ordered = orderMoves(board, moves);
|
|
262
|
+
let best = ordered[0], bestScore = -Infinity, alpha = -Infinity;
|
|
263
|
+
for (const m of ordered) {
|
|
264
|
+
const nb = applyMove(board, m[0], m[1], m[2], m[3]);
|
|
265
|
+
const score = -negamax(nb, enemy, depth - 1, -Infinity, -alpha);
|
|
266
|
+
if (score > bestScore) { bestScore = score; best = m; }
|
|
267
|
+
if (bestScore > alpha) alpha = bestScore;
|
|
268
|
+
}
|
|
269
|
+
return best;
|
|
270
|
+
}
|
|
@@ -52,7 +52,7 @@ export const feature = {
|
|
|
52
52
|
|
|
53
53
|
// ===== 定价与金额估算 =====
|
|
54
54
|
// 定价表以「人民币元 / 百万 tokens」为基准(DeepSeek/智谱官方刊例即人民币), 金额先算 CNY,
|
|
55
|
-
//
|
|
55
|
+
// 对外记录的 cost 保持 USD,供旧版客户端兼容;当前客户端和 CSV 统一展示人民币。
|
|
56
56
|
// usdCnyRate 与 pricing 均可被 settings.yaml 覆盖:
|
|
57
57
|
// dsh-dock-tokenlog:
|
|
58
58
|
// usdCnyRate: 7.2 # USD↔CNY 汇率
|
|
@@ -649,7 +649,7 @@ export const feature = {
|
|
|
649
649
|
if (method === 'export') {
|
|
650
650
|
loadPricingConfig()
|
|
651
651
|
const list = buildQuery(payload || {})
|
|
652
|
-
const header = ['time', 'provider', 'model', 'apiKey', 'inputTokens', 'outputTokens', 'cacheReadTokens', 'cacheWriteTokens', 'reasoningTokens', 'billedInput', 'cacheHitPercent', 'totalTokens', '
|
|
652
|
+
const header = ['time', 'provider', 'model', 'apiKey', 'inputTokens', 'outputTokens', 'cacheReadTokens', 'cacheWriteTokens', 'reasoningTokens', 'billedInput', 'cacheHitPercent', 'totalTokens', 'costCny', 'effort', 'status', 'statusCode', 'errorCode', 'errorMsg', 'llmMs', 'sessionId', 'turn', 'step']
|
|
653
653
|
const esc = (v) => {
|
|
654
654
|
const s = String(v === undefined || v === null ? '' : v)
|
|
655
655
|
return /[,"\n]/.test(s) ? '"' + s.replace(/"/g, '""') + '"' : s
|
|
@@ -37,13 +37,7 @@ function fmtCompact(n) {
|
|
|
37
37
|
if (n >= 1e3) return (n / 1e3).toFixed(1) + "K";
|
|
38
38
|
return String(Math.round(n));
|
|
39
39
|
}
|
|
40
|
-
|
|
41
|
-
const v = Number(n) || 0;
|
|
42
|
-
if (v >= 10) return "$" + v.toFixed(2);
|
|
43
|
-
if (v >= 0.01) return "$" + v.toFixed(4);
|
|
44
|
-
return "$" + v.toFixed(6);
|
|
45
|
-
}
|
|
46
|
-
// 人民币金额: USD 按汇率换算, 默认 7.2(host 返回 rateUsdCny 可覆盖)
|
|
40
|
+
// 金额记录在 RPC 中以 USD 传输;界面统一按人民币显示。
|
|
47
41
|
function fmtCostCny(usd, rate) {
|
|
48
42
|
const v = (Number(usd) || 0) * (Number(rate) > 0 ? Number(rate) : 7.2);
|
|
49
43
|
if (v >= 10000) return "¥" + (v / 10000).toFixed(2) + "万";
|
|
@@ -170,7 +164,7 @@ function Detail({ rec, onClose, rate }) {
|
|
|
170
164
|
["计费输入", fmtNum(rec.billedInput)],
|
|
171
165
|
["缓存命中率", rec.cacheHitPercent + "%"],
|
|
172
166
|
["总 Token", fmtNum(rec.totalTokens)],
|
|
173
|
-
["消耗金额(估算)",
|
|
167
|
+
["消耗金额(估算)", fmtCostCny(rec.cost, rateCny)],
|
|
174
168
|
["推理强度", rec.effort || "—"],
|
|
175
169
|
["耗时", fmtDuration(rec.llmMs)],
|
|
176
170
|
["Turn / Step", rec.turn + " / " + rec.step],
|
|
@@ -350,8 +344,7 @@ export function TokenLogView(props) {
|
|
|
350
344
|
{ v: fmtCompact(totals.cacheReadTokens), l: "缓存命中" },
|
|
351
345
|
{ v: totals.cacheHitPct + "%", l: "缓存命中率" },
|
|
352
346
|
{ v: fmtCompact(totals.outputTokens), l: "输出" },
|
|
353
|
-
{ v:
|
|
354
|
-
{ v: fmtCostCny(totals.cost, rateCny), l: "消耗金额(¥)" },
|
|
347
|
+
{ v: fmtCostCny(totals.cost, rateCny), l: "消耗金额(估算·人民币)" },
|
|
355
348
|
{ v: fmtDuration(totals.llmMs), l: "累计耗时" + (totals.timed ? " (" + totals.timed + "步)" : "") },
|
|
356
349
|
] : [];
|
|
357
350
|
|
|
@@ -368,7 +361,6 @@ export function TokenLogView(props) {
|
|
|
368
361
|
<td>{r.key}</td><td>{fmtNum(r.calls)}</td><td>{fmtCompact(r.inputTokens)}</td>
|
|
369
362
|
<td>{fmtCompact(r.cacheReadTokens)}</td><td>{r.cacheHitPct + "%"}</td><td>{fmtCompact(r.outputTokens)}</td>
|
|
370
363
|
<td>{fmtCompact(r.totalTokens)}</td>
|
|
371
|
-
<td>{fmtCost(r.cost)}</td>
|
|
372
364
|
<td>{fmtCostCny(r.cost, rateCny)}</td>
|
|
373
365
|
<td>{fmtDuration(r.llmMs)}</td>
|
|
374
366
|
</tr>
|
|
@@ -394,7 +386,6 @@ export function TokenLogView(props) {
|
|
|
394
386
|
<td>{fmtNum(r.outputTokens)}</td>
|
|
395
387
|
<td>{fmtNum(r.reasoningTokens)}</td>
|
|
396
388
|
<td>{fmtNum(r.totalTokens)}</td>
|
|
397
|
-
<td>{fmtCost(r.cost)}</td>
|
|
398
389
|
<td>{fmtCostCny(r.cost, rateCny)}</td>
|
|
399
390
|
<td>{r.effort || "—"}</td>
|
|
400
391
|
<td>
|
|
@@ -418,7 +409,7 @@ export function TokenLogView(props) {
|
|
|
418
409
|
<div key="sumtab" className="dtok-table-wrap">
|
|
419
410
|
<table className="dtok-table">
|
|
420
411
|
<thead><tr>
|
|
421
|
-
<th>维度</th><th>调用</th><th>输入</th><th>缓存</th><th>命中率</th><th>输出</th><th>总Token</th><th
|
|
412
|
+
<th>维度</th><th>调用</th><th>输入</th><th>缓存</th><th>命中率</th><th>输出</th><th>总Token</th><th>金额(人民币)</th><th>耗时</th>
|
|
422
413
|
</tr></thead>
|
|
423
414
|
<tbody>{summaryRows}</tbody>
|
|
424
415
|
</table>
|
|
@@ -451,7 +442,7 @@ export function TokenLogView(props) {
|
|
|
451
442
|
<thead><tr>
|
|
452
443
|
{sortTh("时间", "time")}{sortTh("会话ID", "sessionId")}{sortTh("提供商", "provider")}{sortTh("模型", "model")}
|
|
453
444
|
{sortTh("输入", "inputTokens")}{sortTh("缓存", "cacheReadTokens")}{sortTh("命中%", "cacheHitPercent")}{sortTh("输出", "outputTokens")}
|
|
454
|
-
{sortTh("推理", "reasoningTokens")}{sortTh("总额", "totalTokens")}{sortTh("
|
|
445
|
+
{sortTh("推理", "reasoningTokens")}{sortTh("总额", "totalTokens")}{sortTh("金额(人民币)", "cost")}{sortTh("强度", "effort")}
|
|
455
446
|
{sortTh("状态", "status")}{sortTh("耗时", "llmMs")}
|
|
456
447
|
</tr></thead>
|
|
457
448
|
<tbody>{detailRows}</tbody>
|
|
@@ -521,32 +512,32 @@ function TokenLogHomeStat() {
|
|
|
521
512
|
}, []);
|
|
522
513
|
if (!snap.totals) return <span>{snap.err ? "用量查询失败(点击进入查看)" : "正在拉取今日用量…"}</span>;
|
|
523
514
|
const t = snap.totals;
|
|
524
|
-
return <span>今日 {fmtNum(t.calls)} 次调用 · {fmtCompact(t.totalTokens)} Token · {
|
|
515
|
+
return <span>今日 {fmtNum(t.calls)} 次调用 · {fmtCompact(t.totalTokens)} Token · {fmtCostCny(t.cost, snap.rate)}</span>;
|
|
525
516
|
}
|
|
526
517
|
|
|
527
518
|
// ---------- 会话输入区 chip:显示当前会话总 Token(10s 静默刷新),点击打开功能坞并按该会话筛选 ----------
|
|
528
519
|
export function TokenLogChip(props) {
|
|
529
520
|
const sid = (props && props.sessionId) || (props && props.session && props.session.sessionId) || null;
|
|
530
|
-
const [snap, setSnap] = useState({ totals: null, err: "" });
|
|
521
|
+
const [snap, setSnap] = useState({ totals: null, rate: 7.2, err: "" });
|
|
531
522
|
useEffect(() => {
|
|
532
523
|
if (!sid) return;
|
|
533
524
|
let cancel = false;
|
|
534
525
|
const load = () => rpcCall("query", { sessionId: sid })
|
|
535
|
-
.then((d) => { if (!cancel) setSnap({ totals: d && d.totals, err: "" }); })
|
|
536
|
-
.catch((e) => { if (!cancel) setSnap((s) => ({ totals: s.totals, err: String((e && e.message) || e) })); });
|
|
526
|
+
.then((d) => { if (!cancel) setSnap({ totals: d && d.totals, rate: (d && d.rateUsdCny) || 7.2, err: "" }); })
|
|
527
|
+
.catch((e) => { if (!cancel) setSnap((s) => ({ totals: s.totals, rate: s.rate, err: String((e && e.message) || e) })); });
|
|
537
528
|
load();
|
|
538
529
|
const timer = setInterval(load, 10000);
|
|
539
530
|
return () => { cancel = true; clearInterval(timer); };
|
|
540
531
|
}, [sid]);
|
|
541
532
|
if (!sid) return null;
|
|
542
533
|
const t = snap.totals;
|
|
543
|
-
// chip
|
|
534
|
+
// chip 文案用紧凑人民币金额(2 位小数),避免撑爆输入卡工具行;
|
|
544
535
|
// 完整金额在 title 悬浮提示里。
|
|
545
536
|
const label = snap.err && !t
|
|
546
537
|
? (snap.err.includes("重启") ? "用量·需重启宿主" : "用量·失败")
|
|
547
|
-
: (t ? "⛁ " + fmtCompact(t.totalTokens) + (t.cost > 0 ? " ·
|
|
538
|
+
: (t ? "⛁ " + fmtCompact(t.totalTokens) + (t.cost > 0 ? " · " + fmtCostCny(t.cost, snap.rate) : "") : "⛁ …");
|
|
548
539
|
const title = t
|
|
549
|
-
? "本会话 " + fmtNum(t.calls) + " 次调用 · " + fmtNum(t.totalTokens) + " Token · " +
|
|
540
|
+
? "本会话 " + fmtNum(t.calls) + " 次调用 · " + fmtNum(t.totalTokens) + " Token · " + fmtCostCny(t.cost, snap.rate) + "(估算)\n点击在功能坞查看用量记录"
|
|
550
541
|
: (snap.err ? snap.err + "\n" : "") + "点击在功能坞查看用量记录";
|
|
551
542
|
return (
|
|
552
543
|
<button type="button" className={"dockchip" + (snap.err && !t ? " err" : "")} title={title} aria-label="会话用量"
|
package/index.js
CHANGED
|
@@ -19,7 +19,6 @@ import { feature as fVisionProxy } from './features/visionproxy/host.js'
|
|
|
19
19
|
import { feature as fBalance } from './features/balance/host.js'
|
|
20
20
|
import { feature as fTokenlog } from './features/tokenlog/host.js'
|
|
21
21
|
import { feature as fAnimation } from './features/animation/host.js'
|
|
22
|
-
import { feature as fMobileRelay } from './features/mobile-relay/host.js'
|
|
23
22
|
|
|
24
23
|
export const name = 'dsh-dock'
|
|
25
24
|
|
|
@@ -39,7 +38,6 @@ export function apply(ctx) {
|
|
|
39
38
|
fBalance,
|
|
40
39
|
fTokenlog,
|
|
41
40
|
fAnimation,
|
|
42
|
-
fMobileRelay,
|
|
43
41
|
]
|
|
44
42
|
|
|
45
43
|
const state = new Map()
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-dock",
|
|
3
|
-
"version": "0.7.
|
|
3
|
+
"version": "0.7.1",
|
|
4
4
|
"description": "DeepSeek Harness 功能坞插件(dsh-dock):模块化功能中枢——每个功能一个 features/ 模块(可单独提取成包发布,又可经 dockBridge 装回面板)。含用量记录(LLM 调用记账/费用统计)、模型设置、图片理解代理、模型余额、任务动画(8 种氛围动效 + 桌面伙伴 3D 场景、完成通知与提示音、钉钉/飞书推送,开关独立、配置持久化);侧栏入口 + 可拖拽弹窗(最大化/最小化/缩放)。",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "index.js",
|
package/src/client.jsx
CHANGED
|
@@ -24,13 +24,12 @@ import { feature as fHeartbeat } from "../features/heartbeat/view.js";
|
|
|
24
24
|
import { feature as fTheme } from "../features/theme/view.js";
|
|
25
25
|
import { feature as fBalance } from "../features/balance/view.js";
|
|
26
26
|
import { feature as fAnimation } from "../features/animation/view.jsx";
|
|
27
|
-
import { feature as fMobileRelay } from "../features/mobile-relay/view.jsx";
|
|
28
27
|
|
|
29
28
|
const name = "dsh-dock";
|
|
30
|
-
const DOCK_VERSION = "0.7.
|
|
29
|
+
const DOCK_VERSION = "0.7.1";
|
|
31
30
|
|
|
32
31
|
// ---- 内置功能注册表:新功能 = features/<id>/ 加模块 + 这里 import 一行 ----
|
|
33
|
-
const BUILTIN_FEATURES = [fTokenlog, fModelconfig, fHeartbeat, fTheme, fBalance, fAnimation
|
|
32
|
+
const BUILTIN_FEATURES = [fTokenlog, fModelconfig, fHeartbeat, fTheme, fBalance, fAnimation];
|
|
34
33
|
// 规划占位(路线图):接入后移除并建 features/<id>/ 模块
|
|
35
34
|
const PLANNED_FEATURES = [];
|
|
36
35
|
const PLANNED_NOTES = {};
|
|
@@ -186,11 +185,16 @@ const SHELL_CSS = [
|
|
|
186
185
|
// 溢出兼容(两层防护,缺一不可):
|
|
187
186
|
// 1) 宽度上限:宿主 .row 是 flex-wrap:wrap 且换行优先于收缩,chips 一长 tools 整行变宽,
|
|
188
187
|
// 右侧模型选择器/发送键被挤到第二行(左右错位)。宿主 .row 带 container-type:inline-size,
|
|
189
|
-
// 用容器单位 cqw 让上限随输入卡宽度自适应(窄卡少占、宽卡多占);不支持 cqw
|
|
188
|
+
// 用容器单位 cqw 让上限随输入卡宽度自适应(窄卡少占、宽卡多占);不支持 cqw 的老内核退回固定值。
|
|
189
|
+
// 注意:旧上限 min(280px,36cqw) 仍偏宽——标准 780px 输入卡下(.row 内容约 762px),
|
|
190
|
+
// add 钮(28)+工作区选择器(~214)+chips(280) 已与右侧模型选择器+发送键(~345px)合计超宽,
|
|
191
|
+
// 即便 chips 未截断 .row 也会换行把模型选择器/发送键挤到第二行。实测把上限收到 189px
|
|
192
|
+
// (≈25cqw)并使 chips 内距收紧(padding 2px 6px、gap 2px)后,实测 4~6M 用量 + 余额
|
|
193
|
+
// 两 chip 可完整显示且不再换行;超限时仍被省略号截断,完整值在 title 悬浮提示里。
|
|
190
194
|
// 2) 截断:超限部分用省略号截断(完整数值在 title 悬浮提示里),防 chips 凸出输入卡圆角(悬空)。
|
|
191
|
-
".dockchip-row{display:inline-flex;align-items:center;gap:
|
|
192
|
-
"@supports (width:1cqw){.dockchip-row{max-width:min(
|
|
193
|
-
".dockchip{display:inline-flex;align-items:center;gap:5px;cursor:pointer;border:none;background:transparent;color:var(--dsw-alias-label-tertiary);border-radius:8px;padding:2px
|
|
195
|
+
".dockchip-row{display:inline-flex;align-items:center;gap:2px;min-width:0;flex:0 1 auto;overflow:hidden;max-width:189px;}",
|
|
196
|
+
"@supports (width:1cqw){.dockchip-row{max-width:min(189px,25cqw);}}",
|
|
197
|
+
".dockchip{display:inline-flex;align-items:center;gap:5px;cursor:pointer;border:none;background:transparent;color:var(--dsw-alias-label-tertiary);border-radius:8px;padding:2px 6px;font-family:inherit;font-size:11px;line-height:18px;white-space:nowrap;min-width:0;overflow:hidden;transition:background .15s var(--ds-ease-in-out),color .15s var(--ds-ease-in-out);}",
|
|
194
198
|
".dockchip > span{overflow:hidden;text-overflow:ellipsis;white-space:nowrap;min-width:0;}",
|
|
195
199
|
".dockchip:hover{background:var(--dsw-alias-interactive-bg-hover);color:var(--dsw-alias-label-primary);}",
|
|
196
200
|
".dockchip .dockchip-dot{width:6px;height:6px;border-radius:50%;flex:none;}",
|
|
@@ -198,6 +202,13 @@ const SHELL_CSS = [
|
|
|
198
202
|
].join("\n");
|
|
199
203
|
|
|
200
204
|
let dockCssTag = null;
|
|
205
|
+
let initialCssSchedule = null;
|
|
206
|
+
function cancelInitialCssSchedule() {
|
|
207
|
+
if (!initialCssSchedule || typeof window === "undefined") return;
|
|
208
|
+
if (initialCssSchedule.type === "idle" && typeof window.cancelIdleCallback === "function") window.cancelIdleCallback(initialCssSchedule.id);
|
|
209
|
+
if (initialCssSchedule.type === "timer") window.clearTimeout(initialCssSchedule.id);
|
|
210
|
+
initialCssSchedule = null;
|
|
211
|
+
}
|
|
201
212
|
function fullDockCss() {
|
|
202
213
|
const parts = [SHELL_CSS];
|
|
203
214
|
for (const f of BUILTIN_FEATURES) if (f.css) parts.push(f.css);
|
|
@@ -209,6 +220,7 @@ function fullDockCss() {
|
|
|
209
220
|
// 外部功能注册携带样式时可重算更新(外部样式后到)。
|
|
210
221
|
function ensureCss() {
|
|
211
222
|
if (typeof document === "undefined") return;
|
|
223
|
+
cancelInitialCssSchedule();
|
|
212
224
|
try {
|
|
213
225
|
if (!dockCssTag || !dockCssTag.isConnected) {
|
|
214
226
|
if (document.querySelector('style[data-plugin-css="dsh-dock"]')) {
|
|
@@ -224,6 +236,20 @@ function ensureCss() {
|
|
|
224
236
|
console.error("[dsh-dock] ensureCss failed:", e && e.message ? e.message : String(e));
|
|
225
237
|
}
|
|
226
238
|
}
|
|
239
|
+
// 刷新页时宿主内容优先绘制:功能坞样式很大,首帧同步解析会让整页短暂停顿。
|
|
240
|
+
// 首次注入延后到浏览器空闲期;之后的外部模块注册仍走 ensureCss() 立即更新。
|
|
241
|
+
function scheduleInitialCss() {
|
|
242
|
+
if (typeof document === "undefined" || initialCssSchedule || (dockCssTag && dockCssTag.isConnected)) return;
|
|
243
|
+
const run = () => {
|
|
244
|
+
initialCssSchedule = null;
|
|
245
|
+
ensureCss();
|
|
246
|
+
};
|
|
247
|
+
if (typeof window !== "undefined" && typeof window.requestIdleCallback === "function") {
|
|
248
|
+
initialCssSchedule = { type: "idle", id: window.requestIdleCallback(run, { timeout: 350 }) };
|
|
249
|
+
} else {
|
|
250
|
+
initialCssSchedule = { type: "timer", id: setTimeout(run, 48) };
|
|
251
|
+
}
|
|
252
|
+
}
|
|
227
253
|
|
|
228
254
|
// ---- 外部功能变化订阅 hook(弹层/设置页重渲染用) ----
|
|
229
255
|
function useExternalVersion() {
|
|
@@ -277,9 +303,14 @@ function DockModal() {
|
|
|
277
303
|
// lastGeom 记住本页生命周期内最后一次几何,重开弹层时还原
|
|
278
304
|
const [win, setWin] = react.useState(() => ({ mode: "normal", x: null, y: null, w: null, h: null }));
|
|
279
305
|
const dlgRef = react.useRef(null);
|
|
306
|
+
const contentRef = react.useRef(null);
|
|
280
307
|
react.useEffect(() => {
|
|
281
308
|
if (lastGeom.w) setWin({ mode: "normal", x: lastGeom.x, y: lastGeom.y, w: lastGeom.w, h: lastGeom.h });
|
|
282
309
|
}, []);
|
|
310
|
+
// 切换左侧模块时总是从新页面顶部开始,避免把上一页的滚动位置带进小游戏等短页面。
|
|
311
|
+
react.useEffect(() => {
|
|
312
|
+
if (open && contentRef.current) contentRef.current.scrollTop = 0;
|
|
313
|
+
}, [active, open]);
|
|
283
314
|
function beginDrag(e, type) {
|
|
284
315
|
if (win.mode !== "normal" || e.button !== 0) return;
|
|
285
316
|
if (type === "move" && e.target && e.target.closest && e.target.closest("button,select,input")) return;
|
|
@@ -382,7 +413,7 @@ function DockModal() {
|
|
|
382
413
|
react.createElement("span", null, m.name),
|
|
383
414
|
m.planned ? react.createElement("span", { className: "dockm-badge" }, "规划中") : null,
|
|
384
415
|
m.external ? react.createElement("span", { className: "dockm-badge" }, "外部") : null))),
|
|
385
|
-
react.createElement("div", { className: "dockm-content" },
|
|
416
|
+
react.createElement("div", { className: "dockm-content", ref: contentRef },
|
|
386
417
|
react.createElement("div", { className: "dockm-content-head" },
|
|
387
418
|
react.createElement("div", { className: "dockm-name" },
|
|
388
419
|
isHome
|
|
@@ -594,7 +625,7 @@ const ctxRef = { current: null };
|
|
|
594
625
|
|
|
595
626
|
export function apply(ctx) {
|
|
596
627
|
ctxRef.current = ctx;
|
|
597
|
-
|
|
628
|
+
scheduleInitialCss();
|
|
598
629
|
initFeatureState(BUILTIN_FEATURES.concat(PLANNED_FEATURES));
|
|
599
630
|
const slots = ctx.get("slots");
|
|
600
631
|
if (slots === undefined) return;
|
|
@@ -616,21 +647,6 @@ export function apply(ctx) {
|
|
|
616
647
|
slots.inject("conversation.input.left", () => slots.register(
|
|
617
648
|
{ name: "conversation.input.left", id: "dsh-dock-chips", order: 10, label: "功能坞" },
|
|
618
649
|
(zone) => react.createElement(DockChips, Object.assign({}, zone, { ctx: ctxRef.current }))));
|
|
619
|
-
// 手机接力链接使用 fragment,避免配对码进入服务器日志或 Referer。插件加载后立即消费并
|
|
620
|
-
// 清掉 fragment,再自动打开对应功能;DSH 会话本身仍由宿主原生会话服务继续承接。
|
|
621
|
-
try {
|
|
622
|
-
if (typeof window !== "undefined") {
|
|
623
|
-
const match = window.location.hash.match(/(?:^#|&)dsh-mobile-relay=([^&]+)/);
|
|
624
|
-
if (match) {
|
|
625
|
-
const launch = decodeURIComponent(match[1]);
|
|
626
|
-
if (/^[A-Za-z0-9_-]+\.[A-F0-9]{10}$/i.test(launch)) {
|
|
627
|
-
sessionStorage.setItem("dsh-dock/mobile-relay/launch/v1", launch);
|
|
628
|
-
window.history.replaceState(null, "", window.location.pathname + window.location.search);
|
|
629
|
-
setTimeout(() => openPanel("mobile-relay"), 0);
|
|
630
|
-
}
|
|
631
|
-
}
|
|
632
|
-
}
|
|
633
|
-
} catch { /* 私密模式禁用 storage/history 时保持手动入口可用 */ }
|
|
634
650
|
}
|
|
635
651
|
|
|
636
652
|
export const inject = ["timer"];
|
package/src/host-core.js
CHANGED
|
@@ -39,7 +39,10 @@ export const DockConfig = z.object({
|
|
|
39
39
|
})
|
|
40
40
|
|
|
41
41
|
/** 任务动画 effectMode 合法值(客户端动画模式)。 */
|
|
42
|
-
export const ANIMATION_MODES = [
|
|
42
|
+
export const ANIMATION_MODES = [
|
|
43
|
+
'flow', 'breathe', 'ring', 'orbit', 'robot', 'matrix', 'stars', 'aurora', 'space',
|
|
44
|
+
'nebula', 'warp', 'radar', 'constellation', 'fireflies', 'ocean', 'prism', 'circuit', 'gravity', 'lantern',
|
|
45
|
+
]
|
|
43
46
|
|
|
44
47
|
/** 提示音 soundEffect 合法值(客户端音效库键名)。 */
|
|
45
48
|
export const SOUND_EFFECTS = ['chime', 'ding', 'coin', 'bell', 'pulse', 'arp']
|