dsh-task-ledger 0.0.115 → 0.0.116
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/dist/client.js +155 -24
- package/dist/client.js.map +1 -1
- package/dist/host/errors.d.ts +2 -2
- package/dist/host/repositories/trend.repository.d.ts +33 -0
- package/dist/host/repositories/trend.repository.js +92 -0
- package/dist/host/services/stats.service.js +6 -2
- package/dist/host/services/trend.service.d.ts +13 -0
- package/dist/host/services/trend.service.js +82 -0
- package/dist/host/types.d.ts +28 -0
- package/package.json +1 -1
- package/skill/SKILL.md +2 -0
- package/skill/package.json +1 -1
- package/skill/scripts/bin/ledger-darwin-arm64 +0 -0
- package/skill/scripts/bin/ledger-linux-amd64 +0 -0
- package/skill/scripts/bin/ledger-windows-amd64.exe +0 -0
package/dist/host/errors.d.ts
CHANGED
|
@@ -19,8 +19,8 @@ export declare class LedgerDataError extends Error {
|
|
|
19
19
|
export interface DegradedInfo {
|
|
20
20
|
readonly workspaceId: string;
|
|
21
21
|
readonly workspaceTitle: string;
|
|
22
|
-
/**
|
|
23
|
-
readonly op: "list" | "history" | "owners";
|
|
22
|
+
/** 哪个读挂了(skill 子命令;趋势走 `stats --trend`,单独一档便于区分)。 */
|
|
23
|
+
readonly op: "list" | "history" | "owners" | "trend";
|
|
24
24
|
readonly code: LedgerErrorCode;
|
|
25
25
|
/** 人话,可直接上界面(真源:脚本 stderr / records 校验文案)。 */
|
|
26
26
|
readonly message: string;
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import type { LedgerWorkspace } from "../types.js";
|
|
2
|
+
/** 趋势图的窗口(周数):与首页右侧卡位一致(2026-10-17 定稿「近 8 周」)。 */
|
|
3
|
+
export declare const TREND_WEEKS = 8;
|
|
4
|
+
/** 一个人在一个周桶里的三个数(skill 口径:新增按 creator、完成 / 未结按 owner)。 */
|
|
5
|
+
export interface OwnerCounts {
|
|
6
|
+
readonly added: number;
|
|
7
|
+
readonly done: number;
|
|
8
|
+
readonly open: number;
|
|
9
|
+
}
|
|
10
|
+
/** 一个周桶;`from` / `to` 是**本机时区**的日期(含边界)。 */
|
|
11
|
+
export interface TrendBucket {
|
|
12
|
+
readonly from: string;
|
|
13
|
+
readonly to: string;
|
|
14
|
+
/** 该周新建。 */
|
|
15
|
+
readonly added: number;
|
|
16
|
+
/** 该周结案(全部人合计)。 */
|
|
17
|
+
readonly done: number;
|
|
18
|
+
/** 该周结束时仍未结案(全部人合计,含窗口前遗留)。 */
|
|
19
|
+
readonly open: number;
|
|
20
|
+
/** uuid → 按人明细(「只看我的」用;没要 / 该人该周没数据则没有这个 key)。 */
|
|
21
|
+
readonly owners?: Readonly<Record<string, OwnerCounts>>;
|
|
22
|
+
}
|
|
23
|
+
/** 单工作区的趋势。 */
|
|
24
|
+
export interface WorkspaceTrend {
|
|
25
|
+
readonly buckets: readonly TrendBucket[];
|
|
26
|
+
/** 实际有数据的第一天(早于窗口起点则等于窗口起点)。 */
|
|
27
|
+
readonly coverageFrom: string;
|
|
28
|
+
readonly coverageTo: string;
|
|
29
|
+
}
|
|
30
|
+
/** 单工作区趋势(skill `stats --trend week --last 8w --by-owner`;过 1.5s 读缓存)。 */
|
|
31
|
+
export declare function readTrend(workspace: LedgerWorkspace): Promise<WorkspaceTrend>;
|
|
32
|
+
/** `stats --trend` → {@link WorkspaceTrend}(容器级严格:缺字段即坏,不当 0)。 */
|
|
33
|
+
export declare function parseTrend(raw: unknown, where: string): WorkspaceTrend;
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 趋势面取数:近 8 周的「完成 / 未结 / 新增」,**整段桶序列一次拿回**(skill `stats --trend`)。
|
|
3
|
+
*
|
|
4
|
+
* 为什么不再拼现有方法:`history --count-only` 只给单窗口总数、且按**结案时间**过滤(新增取不到);
|
|
5
|
+
* 插件侧拉全量明细再聚合已被实测否决(20 万条 3s+、30MB 过进程边界)。
|
|
6
|
+
* 桶边界(周一起算 / 本机时区)与「未结 = 该周末仍未结案」的倒推算法全在 skill,
|
|
7
|
+
* 本文件只做形状校验 —— 与 stats 同一条纪律:数字真源只有 skill 一份。
|
|
8
|
+
*
|
|
9
|
+
* 形状不符一律抛 {@link LedgerDataError}(不拿 0 充数)。
|
|
10
|
+
*/
|
|
11
|
+
import { LedgerDataError } from "../errors.js";
|
|
12
|
+
import { cached, cacheKey } from "./cache.js";
|
|
13
|
+
import { runLedgerScript } from "./runner.js";
|
|
14
|
+
/** 趋势图的窗口(周数):与首页右侧卡位一致(2026-10-17 定稿「近 8 周」)。 */
|
|
15
|
+
export const TREND_WEEKS = 8;
|
|
16
|
+
const TREND_LAST = `${String(TREND_WEEKS)}w`;
|
|
17
|
+
function isPlainObject(value) {
|
|
18
|
+
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
19
|
+
}
|
|
20
|
+
function num(source, key, where) {
|
|
21
|
+
const value = source[key];
|
|
22
|
+
if (typeof value !== "number" || !Number.isFinite(value))
|
|
23
|
+
throw new LedgerDataError(where, `${where}: stats --trend 输出缺少数字字段 ${key},格式可能已变`);
|
|
24
|
+
return value;
|
|
25
|
+
}
|
|
26
|
+
function day(source, key, where) {
|
|
27
|
+
const value = source[key];
|
|
28
|
+
if (typeof value !== "string" || value === "")
|
|
29
|
+
throw new LedgerDataError(where, `${where}: stats --trend 输出缺少日期字段 ${key},格式可能已变`);
|
|
30
|
+
return value;
|
|
31
|
+
}
|
|
32
|
+
/** 按人明细:没有这个字段(没要 --by-owner)返回 undefined;有就必须是合法的三数对象。 */
|
|
33
|
+
function parseOwners(raw, where) {
|
|
34
|
+
if (raw === undefined)
|
|
35
|
+
return undefined;
|
|
36
|
+
if (!isPlainObject(raw))
|
|
37
|
+
throw new LedgerDataError(where, `${where}: stats --trend 的 owners 不是对象,格式可能已变`);
|
|
38
|
+
const out = {};
|
|
39
|
+
for (const [uuid, cell] of Object.entries(raw)) {
|
|
40
|
+
if (!isPlainObject(cell))
|
|
41
|
+
throw new LedgerDataError(where, `${where}: stats --trend 里 ${uuid} 的按人明细不是对象`);
|
|
42
|
+
out[uuid] = {
|
|
43
|
+
added: num(cell, "added", where),
|
|
44
|
+
done: num(cell, "done", where),
|
|
45
|
+
open: num(cell, "open", where),
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
return out;
|
|
49
|
+
}
|
|
50
|
+
/** 单工作区趋势(skill `stats --trend week --last 8w --by-owner`;过 1.5s 读缓存)。 */
|
|
51
|
+
export async function readTrend(workspace) {
|
|
52
|
+
// 缓存 op 仍记 "stats"(脚本子命令就是 stats),detail 里写清是哪个窗口的趋势
|
|
53
|
+
return await cached(cacheKey(workspace.id, "stats", `trend:${TREND_LAST}`), async () => {
|
|
54
|
+
// 趋势自带 JSON 输出,**不能**再传 --json(skill 那边是用法错)
|
|
55
|
+
const result = await runLedgerScript("stats",
|
|
56
|
+
// --by-owner:一次就把「全部」与「按人」两套数都拿回来 ⇒ 勾「只看我的」零请求
|
|
57
|
+
["--repo", workspace.path, "--trend", "week", "--last", TREND_LAST, "--by-owner"],
|
|
58
|
+
// 8 周窗口最坏要读 3 个历史文件,比单月读重:给到 20s 再判超时
|
|
59
|
+
{ timeoutMs: 20_000 });
|
|
60
|
+
if (!result.ok)
|
|
61
|
+
throw new Error(`${workspace.title}: ${result.error}`);
|
|
62
|
+
return parseTrend(result.data, workspace.title);
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
/** `stats --trend` → {@link WorkspaceTrend}(容器级严格:缺字段即坏,不当 0)。 */
|
|
66
|
+
export function parseTrend(raw, where) {
|
|
67
|
+
if (!isPlainObject(raw))
|
|
68
|
+
throw new LedgerDataError(where, `${where}: stats --trend 输出不是对象,格式可能已变`);
|
|
69
|
+
const coverage = raw.coverage;
|
|
70
|
+
if (!isPlainObject(coverage))
|
|
71
|
+
throw new LedgerDataError(where, `${where}: stats --trend 输出缺 coverage,无法判断数据从哪天起有效`);
|
|
72
|
+
if (!Array.isArray(raw.buckets))
|
|
73
|
+
throw new LedgerDataError(where, `${where}: stats --trend 输出缺 buckets 数组,格式可能已变`);
|
|
74
|
+
const buckets = raw.buckets.map((item) => {
|
|
75
|
+
if (!isPlainObject(item))
|
|
76
|
+
throw new LedgerDataError(where, `${where}: stats --trend 的桶不是对象`);
|
|
77
|
+
const owners = parseOwners(item.owners, where);
|
|
78
|
+
return {
|
|
79
|
+
from: day(item, "from", where),
|
|
80
|
+
to: day(item, "to", where),
|
|
81
|
+
added: num(item, "added", where),
|
|
82
|
+
done: num(item, "done", where),
|
|
83
|
+
open: num(item, "open", where),
|
|
84
|
+
...(owners === undefined ? {} : { owners }),
|
|
85
|
+
};
|
|
86
|
+
});
|
|
87
|
+
return {
|
|
88
|
+
buckets,
|
|
89
|
+
coverageFrom: day(coverage, "from", where),
|
|
90
|
+
coverageTo: day(coverage, "to", where),
|
|
91
|
+
};
|
|
92
|
+
}
|
|
@@ -3,10 +3,11 @@
|
|
|
3
3
|
*
|
|
4
4
|
* 数字真源全在 skill:每工作区跑 `stats --json`(在办各面)与两次
|
|
5
5
|
* `history --count-only`(本周 / 上周完成),本文件只做跨工作区求和与降级归集。
|
|
6
|
-
* 趋势图(近 8
|
|
6
|
+
* 趋势图(近 8 周)由 `trend.service.ts` 走 `stats --trend` 单独算,随首屏一起打包回来。
|
|
7
7
|
*/
|
|
8
8
|
import { codeOf, messageOf, settleWorkspaces, } from "../errors.js";
|
|
9
9
|
import { getCurrentIdentity } from "../repositories/identity.repository.js";
|
|
10
|
+
import { getBoardTrend } from "./trend.service.js";
|
|
10
11
|
import { readOwners, readTasks } from "../repositories/ledger.repository.js";
|
|
11
12
|
import { countClosed, readStats, } from "../repositories/stats.repository.js";
|
|
12
13
|
import { weekWindow } from "./dates.js";
|
|
@@ -60,10 +61,11 @@ function dedupe(rows) {
|
|
|
60
61
|
* 三条读并行(`stats` / `list` / `owners`),各自缓存;一个工作区坏只进 degraded,其余照常。
|
|
61
62
|
*/
|
|
62
63
|
export async function getBoardHome(workspaces, now = new Date()) {
|
|
63
|
-
const [statsPayload, taskScope, ownerScope] = await Promise.all([
|
|
64
|
+
const [statsPayload, taskScope, ownerScope, trend] = await Promise.all([
|
|
64
65
|
getBoardStats(workspaces, now),
|
|
65
66
|
settleWorkspaces(workspaces, "list", readTaskScope, (rows) => rows),
|
|
66
67
|
settleWorkspaces(workspaces, "owners", readOwners, (rows) => rows),
|
|
68
|
+
getBoardTrend(workspaces),
|
|
67
69
|
]);
|
|
68
70
|
const identities = {};
|
|
69
71
|
const tasks = [];
|
|
@@ -73,6 +75,7 @@ export async function getBoardHome(workspaces, now = new Date()) {
|
|
|
73
75
|
}
|
|
74
76
|
return {
|
|
75
77
|
...statsPayload,
|
|
78
|
+
trend: { buckets: trend.buckets, coverageFrom: trend.coverageFrom, coverageTo: trend.coverageTo },
|
|
76
79
|
workspaces,
|
|
77
80
|
tasks,
|
|
78
81
|
identities,
|
|
@@ -81,6 +84,7 @@ export async function getBoardHome(workspaces, now = new Date()) {
|
|
|
81
84
|
...statsPayload.degraded,
|
|
82
85
|
...taskScope.degraded,
|
|
83
86
|
...ownerScope.degraded,
|
|
87
|
+
...trend.degraded,
|
|
84
88
|
]),
|
|
85
89
|
};
|
|
86
90
|
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { type TrendBucket } from "../repositories/trend.repository.js";
|
|
2
|
+
import type { LedgerWorkspace, TrendPayload, TrendView } from "../types.js";
|
|
3
|
+
/**
|
|
4
|
+
* 跨工作区求和:同一天起止的周桶相加。
|
|
5
|
+
* 工作区之间周界一致(同机同时区、都由 skill 的周一起算),所以直接按 `from` 归并。
|
|
6
|
+
*/
|
|
7
|
+
export declare function mergeTrend(rows: readonly {
|
|
8
|
+
buckets: readonly TrendBucket[];
|
|
9
|
+
coverageFrom: string;
|
|
10
|
+
coverageTo: string;
|
|
11
|
+
}[]): TrendView;
|
|
12
|
+
/** 趋势图数据:逐工作区读、坏的进 degraded,全挂给空桶(不抛)。 */
|
|
13
|
+
export declare function getBoardTrend(workspaces: readonly LedgerWorkspace[]): Promise<TrendPayload>;
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 首页趋势图的用例:跨工作区把「近 8 周」的桶**按周求和**。
|
|
3
|
+
*
|
|
4
|
+
* 数字真源全在 skill(每工作区 `stats --trend week --last 8w`),本文件只做两件事:
|
|
5
|
+
* 按 `from` 把各工作区同周的数加在一起、把读挂的工作区归进 `degraded`。
|
|
6
|
+
*
|
|
7
|
+
* ⚠️ 这里**不抛**「全部失败」:趋势只是首页的一块图,不能因为它把 8 张卡和列表一起打成 500。
|
|
8
|
+
* 全挂时返回空桶 + inherited degraded,界面显示空态(真源口径见 errors.ts 的 settleWorkspaces 注释)。
|
|
9
|
+
*/
|
|
10
|
+
import { codeOf, messageOf } from "../errors.js";
|
|
11
|
+
import { readTrend } from "../repositories/trend.repository.js";
|
|
12
|
+
const byFrom = (a, b) => a.from < b.from ? -1 : a.from > b.from ? 1 : 0;
|
|
13
|
+
/**
|
|
14
|
+
* 跨工作区求和:同一天起止的周桶相加。
|
|
15
|
+
* 工作区之间周界一致(同机同时区、都由 skill 的周一起算),所以直接按 `from` 归并。
|
|
16
|
+
*/
|
|
17
|
+
export function mergeTrend(rows) {
|
|
18
|
+
const merged = new Map();
|
|
19
|
+
let coverageFrom = "";
|
|
20
|
+
let coverageTo = "";
|
|
21
|
+
for (const row of rows) {
|
|
22
|
+
for (const bucket of row.buckets) {
|
|
23
|
+
let current = merged.get(bucket.from);
|
|
24
|
+
if (current === undefined) {
|
|
25
|
+
current = {
|
|
26
|
+
from: bucket.from,
|
|
27
|
+
to: bucket.to,
|
|
28
|
+
added: 0,
|
|
29
|
+
done: 0,
|
|
30
|
+
open: 0,
|
|
31
|
+
owners: {},
|
|
32
|
+
};
|
|
33
|
+
merged.set(bucket.from, current);
|
|
34
|
+
}
|
|
35
|
+
// 已有对象就原地累加(同一份数据只在本函数里生成,不会被外部共享)
|
|
36
|
+
current.added += bucket.added;
|
|
37
|
+
current.done += bucket.done;
|
|
38
|
+
current.open += bucket.open;
|
|
39
|
+
for (const [uuid, cell] of Object.entries(bucket.owners ?? {})) {
|
|
40
|
+
const owner = current.owners[uuid] ?? { added: 0, done: 0, open: 0 };
|
|
41
|
+
current.owners[uuid] = {
|
|
42
|
+
added: owner.added + cell.added,
|
|
43
|
+
done: owner.done + cell.done,
|
|
44
|
+
open: owner.open + cell.open,
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
// 有数据的第一天取各工作区里最早的;最后一天取最晚的
|
|
49
|
+
if (row.coverageFrom !== "") {
|
|
50
|
+
if (coverageFrom === "" || row.coverageFrom < coverageFrom)
|
|
51
|
+
coverageFrom = row.coverageFrom;
|
|
52
|
+
}
|
|
53
|
+
if (row.coverageTo !== "" && row.coverageTo > coverageTo)
|
|
54
|
+
coverageTo = row.coverageTo;
|
|
55
|
+
}
|
|
56
|
+
return {
|
|
57
|
+
buckets: [...merged.values()].sort(byFrom),
|
|
58
|
+
coverageFrom,
|
|
59
|
+
coverageTo,
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
/** 趋势图数据:逐工作区读、坏的进 degraded,全挂给空桶(不抛)。 */
|
|
63
|
+
export async function getBoardTrend(workspaces) {
|
|
64
|
+
const settled = await Promise.allSettled(workspaces.map((workspace) => readTrend(workspace)));
|
|
65
|
+
const rows = [];
|
|
66
|
+
const degraded = [];
|
|
67
|
+
settled.forEach((result, i) => {
|
|
68
|
+
if (result.status === "fulfilled") {
|
|
69
|
+
rows.push(result.value);
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
const workspace = workspaces[i];
|
|
73
|
+
degraded.push({
|
|
74
|
+
workspaceId: workspace.id,
|
|
75
|
+
workspaceTitle: workspace.title,
|
|
76
|
+
op: "trend",
|
|
77
|
+
code: codeOf(result.reason),
|
|
78
|
+
message: messageOf(result.reason),
|
|
79
|
+
});
|
|
80
|
+
});
|
|
81
|
+
return { ...mergeTrend(rows), degraded };
|
|
82
|
+
}
|
package/dist/host/types.d.ts
CHANGED
|
@@ -114,6 +114,32 @@ export interface StatsPayload {
|
|
|
114
114
|
readonly stats: BoardStatsView;
|
|
115
115
|
readonly degraded: readonly DegradedInfo[];
|
|
116
116
|
}
|
|
117
|
+
/** 一个人在一个周桶里的三个数(新增按 creator、完成 / 未结按 owner,都是 skill 的口径)。 */
|
|
118
|
+
export interface TrendOwnerCounts {
|
|
119
|
+
readonly added: number;
|
|
120
|
+
readonly done: number;
|
|
121
|
+
readonly open: number;
|
|
122
|
+
}
|
|
123
|
+
/** 趋势图的一根周桶(数字全由 skill `stats --trend --by-owner` 给,插件只跨工作区相加)。 */
|
|
124
|
+
export interface TrendBucketView {
|
|
125
|
+
readonly from: string;
|
|
126
|
+
readonly to: string;
|
|
127
|
+
readonly added: number;
|
|
128
|
+
readonly done: number;
|
|
129
|
+
readonly open: number;
|
|
130
|
+
/** uuid → 按人明细;「只看我的」直接取自己那条,勾选零请求。 */
|
|
131
|
+
readonly owners: Readonly<Record<string, TrendOwnerCounts>>;
|
|
132
|
+
}
|
|
133
|
+
/** 趋势图数据:近 8 周桶序列 + 实际有数据的第一天。 */
|
|
134
|
+
export interface TrendView {
|
|
135
|
+
readonly buckets: readonly TrendBucketView[];
|
|
136
|
+
readonly coverageFrom: string;
|
|
137
|
+
readonly coverageTo: string;
|
|
138
|
+
}
|
|
139
|
+
/** 趋势数据 + 降级(自带一条,免得拖垮整个快照)。 */
|
|
140
|
+
export interface TrendPayload extends TrendView {
|
|
141
|
+
readonly degraded: readonly DegradedInfo[];
|
|
142
|
+
}
|
|
117
143
|
/** 一个工作区的花名册(skill `owners --json`:含当前没任务的人)。 */
|
|
118
144
|
export interface WorkspaceOwners {
|
|
119
145
|
readonly workspaceId: string;
|
|
@@ -130,6 +156,8 @@ export interface WorkspaceOwners {
|
|
|
130
156
|
* 前端只筛不判。
|
|
131
157
|
*/
|
|
132
158
|
export interface BoardHomePayload extends StatsPayload {
|
|
159
|
+
/** 首页右侧趋势图(近 8 周)。 */
|
|
160
|
+
readonly trend: TrendView;
|
|
133
161
|
readonly workspaces: readonly LedgerWorkspace[];
|
|
134
162
|
readonly tasks: readonly TaskRow[];
|
|
135
163
|
readonly identities: Readonly<Record<string, string | null>>;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-task-ledger",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.116",
|
|
4
4
|
"description": "Task ledger board for dsh (DeepSeek Harness): read-only board over task-ledger skill workspaces. The task-ledger skill is bundled — install the plugin, get the skill globally.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"deepseek",
|
package/skill/SKILL.md
CHANGED
|
@@ -166,6 +166,8 @@ allowed-tools: Read,Edit,Bash,Grep
|
|
|
166
166
|
|
|
167
167
|
口径:`added` 按 `created`、`done` 按 `finished`;`open` 由「当前在办 + 今天往前倒推」得出,天然含窗口之前遗留的基线。按人明细里 `added` 按 `creator`、`done` / `open` 按 `owner`(待认领不算任何人)。
|
|
168
168
|
|
|
169
|
+
按人的 `open` 只算**你名下**的任务,倒推时同时减去「你在那天之后新建的」(按 owner 计,与展示用的 creator 无关)—— 否则窗口之后才建、早已结案的任务会被当成那天就在办,越靠前的桶虚高得越离谱。负责人转交过的任务按**今天**的负责人归账。
|
|
170
|
+
|
|
169
171
|
**只出 JSON**(表格由界面渲染;给 `--json` 是用法错):
|
|
170
172
|
|
|
171
173
|
```json
|
package/skill/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "task-ledger",
|
|
3
|
-
"version": "1.3.
|
|
3
|
+
"version": "1.3.42",
|
|
4
4
|
"description": "Task-ledger skill for coding agents: per-workspace progress tracking (ledger/pool.json + owners/<hash>.json shards + owners.json index, history, worklog) driven by a CLI script. Standalone install for CodeBuddy and other agents; also bundled inside the dsh-task-ledger DSH plugin.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"skill",
|
|
Binary file
|
|
Binary file
|
|
Binary file
|