@v587d/capital-generation 2.8.2 → 2.9.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/CHANGELOG.md +87 -1
- package/README.md +41 -20
- package/chart-ui/README.md +1 -1
- package/chart-ui/client.src.cjs +7 -16
- package/chart-ui/index.js +12 -33
- package/chart-ui/package.json +2 -2
- package/config/client.js +18 -39
- package/config/client.src.cjs +79 -217
- package/config/index.js +22 -72
- package/config/package.json +2 -2
- package/cordis.patch.yml +46 -51
- package/lib/agents/bash-guard.d.ts +9 -55
- package/lib/agents/bash-guard.js +27 -67
- package/lib/agents/preset-roster.d.ts +22 -0
- package/lib/agents/preset-roster.js +87 -0
- package/lib/agents/role-tool-policy.d.ts +46 -0
- package/lib/agents/role-tool-policy.js +205 -0
- package/lib/agents/root-tool-policy.d.ts +28 -124
- package/lib/agents/root-tool-policy.js +164 -237
- package/lib/agents/selected-skills-provider.d.ts +2 -6
- package/lib/agents/selected-skills-provider.js +27 -84
- package/lib/capital-settings.d.ts +27 -0
- package/lib/capital-settings.js +23 -0
- package/lib/chart/artifact-ref.d.ts +3 -8
- package/lib/chart/artifact-ref.js +3 -8
- package/lib/chart/downsample.d.ts +2 -5
- package/lib/chart/downsample.js +3 -8
- package/lib/chart/errors.d.ts +3 -7
- package/lib/chart/errors.js +1 -2
- package/lib/chart/event-context.d.ts +8 -0
- package/lib/chart/event-context.js +39 -0
- package/lib/chart/events.d.ts +21 -78
- package/lib/chart/events.js +40 -189
- package/lib/chart/html.d.ts +2 -5
- package/lib/chart/html.js +8 -15
- package/lib/chart/runtime.d.ts +5 -18
- package/lib/chart/runtime.js +14 -34
- package/lib/chart/series.d.ts +10 -17
- package/lib/chart/series.js +22 -47
- package/lib/chart/source-token.d.ts +5 -13
- package/lib/chart/source-token.js +18 -17
- package/lib/chart/spec.d.ts +6 -12
- package/lib/chart/spec.js +7 -31
- package/lib/chart/tool.d.ts +6 -29
- package/lib/chart/tool.js +9 -23
- package/lib/chart-render-entry.d.ts +9 -0
- package/lib/chart-render-entry.js +17 -0
- package/lib/chart-source-entry.d.ts +13 -0
- package/lib/chart-source-entry.js +20 -0
- package/lib/collect-entry.d.ts +9 -0
- package/lib/collect-entry.js +10 -0
- package/lib/collector-runtime.d.ts +22 -0
- package/lib/collector-runtime.js +97 -0
- package/lib/data-collector/args.d.ts +6 -24
- package/lib/data-collector/args.js +6 -24
- package/lib/data-collector/dataset-tools.d.ts +7 -1
- package/lib/data-collector/dataset-tools.js +27 -56
- package/lib/data-collector/describe.d.ts +8 -33
- package/lib/data-collector/describe.js +17 -70
- package/lib/data-collector/hub.d.ts +27 -85
- package/lib/data-collector/hub.js +19 -85
- package/lib/data-collector/query-time.d.ts +7 -26
- package/lib/data-collector/query-time.js +8 -27
- package/lib/data-collector/store.d.ts +40 -117
- package/lib/data-collector/store.js +32 -111
- package/lib/data-collector/time-axis.d.ts +16 -76
- package/lib/data-collector/time-axis.js +25 -111
- package/lib/data-collector/tools.d.ts +6 -6
- package/lib/data-collector/tools.js +34 -43
- package/lib/dataset-read-entry.d.ts +9 -0
- package/lib/dataset-read-entry.js +7 -0
- package/lib/index.d.ts +7 -117
- package/lib/index.js +43 -422
- package/lib/net/eastmoney-client.d.ts +13 -65
- package/lib/net/eastmoney-client.js +12 -53
- package/lib/net/throttle.d.ts +2 -13
- package/lib/net/throttle.js +3 -14
- package/lib/ocr/client.d.ts +12 -21
- package/lib/ocr/client.js +23 -40
- package/lib/ocr/markdown.d.ts +20 -26
- package/lib/ocr/markdown.js +23 -36
- package/lib/ocr/store.d.ts +14 -47
- package/lib/ocr/store.js +15 -45
- package/lib/ocr-entry.d.ts +9 -0
- package/lib/ocr-entry.js +10 -0
- package/lib/policy-entry.d.ts +17 -0
- package/lib/policy-entry.js +22 -0
- package/lib/preset-claim.d.ts +22 -0
- package/lib/preset-claim.js +24 -0
- package/lib/preset-ids.d.ts +38 -0
- package/lib/preset-ids.js +46 -0
- package/lib/retriever-config.d.ts +37 -0
- package/lib/retriever-config.js +36 -0
- package/lib/retriever-runtime.d.ts +31 -0
- package/lib/retriever-runtime.js +82 -0
- package/lib/root-extras-entry.d.ts +17 -0
- package/lib/root-extras-entry.js +25 -0
- package/lib/runtime-entry.d.ts +15 -0
- package/lib/runtime-entry.js +5 -0
- package/lib/runtime.d.ts +21 -0
- package/lib/runtime.js +33 -0
- package/lib/search-entry.d.ts +9 -0
- package/lib/search-entry.js +9 -0
- package/lib/sources/eastmoney-http.js +20 -68
- package/lib/sources/ftshare-rest.d.ts +6 -31
- package/lib/sources/ftshare-rest.js +1194 -225
- package/lib/sources/fuyao-core.d.ts +8 -18
- package/lib/sources/fuyao-core.js +10 -21
- package/lib/sources/fuyao-rest.d.ts +2 -6
- package/lib/sources/fuyao-rest.js +23 -102
- package/lib/sources/security-code.d.ts +6 -12
- package/lib/sources/security-code.js +6 -12
- package/lib/sources/security-identity.d.ts +1 -4
- package/lib/sources/security-identity.js +3 -8
- package/lib/sources/tencent-http.js +14 -28
- package/lib/sources/tencent-public-core.d.ts +8 -60
- package/lib/sources/tencent-public-core.js +12 -77
- package/lib/sources/wind-mcp.d.ts +0 -5
- package/lib/sources/wind-mcp.js +23 -78
- package/lib/sources-entry.d.ts +9 -0
- package/lib/sources-entry.js +9 -0
- package/lib/time/tools.d.ts +8 -31
- package/lib/time/tools.js +39 -64
- package/lib/time-entry.d.ts +9 -0
- package/lib/time-entry.js +7 -0
- package/lib/tool-exec.d.ts +45 -11
- package/lib/tool-exec.js +59 -6
- package/lib/user-customization.d.ts +14 -0
- package/lib/user-customization.js +47 -0
- package/lib/watchlist/tools.d.ts +3 -13
- package/lib/watchlist/tools.js +10 -30
- package/lib/watchlist-entry.d.ts +9 -0
- package/lib/watchlist-entry.js +6 -0
- package/lib/web-retriever/engines.d.ts +6 -11
- package/lib/web-retriever/engines.js +9 -14
- package/lib/web-retriever/html-markdown.d.ts +14 -34
- package/lib/web-retriever/html-markdown.js +39 -75
- package/lib/web-retriever/local-fetch.d.ts +22 -58
- package/lib/web-retriever/local-fetch.js +33 -77
- package/lib/web-retriever/retriever.d.ts +6 -18
- package/lib/web-retriever/retriever.js +7 -7
- package/lib/web-retriever/sources.d.ts +37 -124
- package/lib/web-retriever/sources.js +64 -196
- package/lib/web-retriever/tools.d.ts +43 -27
- package/lib/web-retriever/tools.js +99 -137
- package/lib/web-retriever/wind-client.d.ts +14 -31
- package/lib/web-retriever/wind-client.js +16 -32
- package/locale/zh.json +1 -1
- package/package.json +67 -7
- package/preset/capital-generation/README.md +87 -53
- package/preset/capital-generation/agent.patch.yml +526 -337
- package/preset/capital-generation/skills/capital-data-profile/SKILL.md +2 -2
- package/preset/capital-generation/skills/capital-data-protocol/SKILL.md +1 -1
- package/preset/capital-generation/skills/capital-equity-research/SKILL.md +1 -1
- package/preset/capital-generation/skills/capital-event-impact/SKILL.md +1 -1
- package/preset/capital-generation/skills/capital-orchestration/SKILL.md +15 -19
- package/preset/capital-generation/skills/capital-portfolio-review/SKILL.md +1 -1
- package/preset/capital-generation/skills/capital-visualization-protocol/SKILL.md +9 -12
- package/preset/capital-generation/skills/capital-web-evidence/SKILL.md +1 -1
- package/watchlist/client.js +15 -148
- package/watchlist/client.src.cjs +148 -662
- package/watchlist/holding-rules.js +11 -38
- package/watchlist/index.js +127 -397
- package/watchlist/package.json +2 -2
package/config/client.src.cjs
CHANGED
|
@@ -14,37 +14,18 @@ const {
|
|
|
14
14
|
const { SELECTED_SKILL_CATALOG, SELECTED_SKILL_TAG_LABELS, SELECTED_SKILLS_MAX_ENABLED } = require('../selected-skills/catalog.js')
|
|
15
15
|
|
|
16
16
|
/**
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
* `src/index.ts` 的 `CAPITAL_CONFIG_ENTRY_ID`、这里。
|
|
22
|
-
* - `BUNDLE_NAME` 是槽 `plugins.bundle.config` 的 key:页面按**组合包的包名**取该包的配置段
|
|
23
|
-
* (`renderSlot(..., { entryKey: pkg.name })`),所以它必须逐字等于根 `package.json` 的 `name`。
|
|
24
|
-
* 它还被 `installStyles` 插进那两条限定"只在我们自己那一页动宿主的组件段"的选择器里 ⇒
|
|
25
|
-
* 包名这条链现在管着**三处**(槽 key、页面选择器、拼出来的 npm 地址)。
|
|
26
|
-
*
|
|
27
|
-
* 漂移的表现都不是报错,而是**卡片静默消失**;两处对齐由 `test/capital-config.test.mjs` 钉住。
|
|
28
|
-
*
|
|
29
|
-
* 为什么不用行级槽 `plugins.row.config`(key `<包名>#<行 id>`):那把座位在「包含的组件」里再往
|
|
30
|
-
* 下一层,填密钥要点三下;且同包的 `capital-charts` / `preset-capital-generation` 没有表单,
|
|
31
|
-
* 点进去是死路。配置是「这个插件」的属性,组合包层才是它该坐的层。
|
|
17
|
+
* Two different strings: `ENTRY_ID` is the settings namespace (= profile entry
|
|
18
|
+
* id, four places in lockstep); `BUNDLE_NAME` is the package name used as the
|
|
19
|
+
* `plugins.bundle.config` key, in selectors and the npm URL. Drift silently
|
|
20
|
+
* removes the card; tests pin both.
|
|
32
21
|
*/
|
|
33
22
|
const ENTRY_ID = 'capital-config'
|
|
34
23
|
const BUNDLE_NAME = '@v587d/capital-generation'
|
|
35
|
-
/** 文案字典的命名空间(与配置条目 id 是两回事,按官方 settings 页的 `settings.*` 惯例)。 */
|
|
36
24
|
const LOCALE_NS = 'settings.capital'
|
|
37
25
|
|
|
38
26
|
/**
|
|
39
|
-
*
|
|
40
|
-
*
|
|
41
|
-
* 注册必须带 `id`(同 id 二次注册直接抛错),且组件收到的是 owner props `subject`——
|
|
42
|
-
* 同一格在**行详情页**与**官方插件页**也会渲染,所以 `subject.kind` 与包名两道都要过,
|
|
43
|
-
* 否则会在别人的页面上挂自己的链接。
|
|
44
|
-
*
|
|
45
|
-
* 两个地址的真值各只有一处:`REPO_URL` 逐字等于根 `package.json` 的 `repository.url`(去掉
|
|
46
|
-
* `git+` 前缀与 `.git` 后缀),`NPM_URL` 由 `BUNDLE_NAME` 拼出。两条同字链由
|
|
47
|
-
* `test/capital-config.test.mjs` 钉住。
|
|
27
|
+
* `plugins.detail.badge` (list, root scope) needs an `id` and also renders on row
|
|
28
|
+
* detail pages, so both `subject.kind` and the package name must gate the links.
|
|
48
29
|
*/
|
|
49
30
|
const BADGE_SLOT = 'plugins.detail.badge'
|
|
50
31
|
const REPO_URL = 'https://github.com/v587d/capital-generation'
|
|
@@ -55,17 +36,9 @@ const FTSHARE_DEFAULT_REF = 'FTSHARE_API_KEY'
|
|
|
55
36
|
const ANYSEARCH_DEFAULT_REF = 'ANYSEARCH_API_KEY'
|
|
56
37
|
const WIND_DEFAULT_REF = 'WIND_API_KEY'
|
|
57
38
|
const PADDLE_OCR_DEFAULT_REF = 'PADDLE_OCR_TOKEN'
|
|
58
|
-
/**
|
|
59
|
-
* 三格 tab 的基 id:官方 `SegmentedControl` 按 `<id>-<value>` 画 tab、按 `<id>-<value>-panel`
|
|
60
|
-
* 发 `aria-controls`(契约写在它的 `.d.ts` 里),所以三块面板的 `id` 必须逐字跟着这两个模板长。
|
|
61
|
-
*/
|
|
39
|
+
/** SegmentedControl contract: tab id is `<id>-<value>`, panel id is `<id>-<value>-panel`. */
|
|
62
40
|
const TABS_ID = 'capital-config-tabs'
|
|
63
|
-
/**
|
|
64
|
-
* 「详情」悬浮里那份 `SKILL.md` 原文的来路:host 半边(`config/index.js`)注册的
|
|
65
|
-
* 只读旁路,按 catalog 的 skill 名寻址——闭集在服务器那侧,这里只拼地址。
|
|
66
|
-
* 这条字符串与 host 的 `SKILL_DOC_ROUTE` 同字(客户端 import 不到那一层,所以由测试对着
|
|
67
|
-
* 两侧产物比):漂移的表现是「详情」永远读不到,且不报错。
|
|
68
|
-
*/
|
|
41
|
+
/** Must match `SKILL_DOC_ROUTE` in `config/index.js`; drift = details never load. */
|
|
69
42
|
const SKILL_DOC_ROUTE = '/capital-skills'
|
|
70
43
|
const DOC_URLS = {
|
|
71
44
|
fuyao: 'https://fuyao.aicubes.cn/docs/',
|
|
@@ -75,11 +48,7 @@ const DOC_URLS = {
|
|
|
75
48
|
paddleocr: 'https://aistudio.baidu.com/paddleocr',
|
|
76
49
|
}
|
|
77
50
|
|
|
78
|
-
/**
|
|
79
|
-
* 密钥字段清单(渲染顺序 = 数组顺序)。读状态、寻 ref 名、写草稿、事件刷新都遍历这一份:
|
|
80
|
-
* 加一个 Key 以前要在九处各补一遍,漏掉的那处不会报错,只会让用户填了却不生效。
|
|
81
|
-
* FTShare 排在 Fuyao 之后:两家是同一格里的两个结构化数据上游(互为第二家),检索与解析跟在后面。
|
|
82
|
-
*/
|
|
51
|
+
/** Render order = array order; one list drives status, refs, drafts and event refresh. */
|
|
83
52
|
const CREDENTIAL_FIELDS = ['fuyao', 'ftshare', 'anysearch', 'wind', 'paddleocr']
|
|
84
53
|
const SELECTED_SKILL_FIELDS = SELECTED_SKILL_CATALOG.map(({ key }) => key)
|
|
85
54
|
const DEFAULT_REFS = {
|
|
@@ -100,24 +69,22 @@ const zh = {
|
|
|
100
69
|
componentsTitle: '包含的组件',
|
|
101
70
|
componentsHint: '组件清单由 DSH 绘制,显示在下方。',
|
|
102
71
|
fuyaoLabel: 'Fuyao API Key',
|
|
103
|
-
fuyaoHint: '同花顺 Fuyao
|
|
72
|
+
fuyaoHint: '同花顺 Fuyao 结构化数据接口。保存后新自动模式会话生效。',
|
|
104
73
|
ftshareLabel: 'FTShare API Key',
|
|
105
|
-
ftshareHint: '非凸科技 FTShare
|
|
74
|
+
ftshareHint: '非凸科技 FTShare 结构化数据接口(免费档)。保存后新自动模式会话生效。',
|
|
106
75
|
anysearchLabel: 'AnySearch API Key',
|
|
107
|
-
anysearchHint: 'AnySearch 网页搜索 /
|
|
76
|
+
anysearchHint: 'AnySearch 网页搜索 / 提取接口。保存后新自动模式会话生效。',
|
|
108
77
|
localFetchLabel: '允许启动本地提取网页内容',
|
|
109
|
-
localFetchHint: '开启后本地抓取兜底(回执来源 local-http
|
|
78
|
+
localFetchHint: '开启后本地抓取兜底(回执来源 local-http)。保存后新自动模式会话生效。',
|
|
110
79
|
windLabel: 'Wind Alice API Key',
|
|
111
|
-
windHint: 'Wind
|
|
80
|
+
windHint: 'Wind 金融信披类文档检索接口。保存后新自动模式会话生效。',
|
|
112
81
|
paddleocrLabel: 'PaddleOCR 文档解析 Token',
|
|
113
|
-
paddleocrHint: '配置 Token 后自动解析 PDF
|
|
82
|
+
paddleocrHint: '配置 Token 后自动解析 PDF、图片。保存后新自动模式会话生效。',
|
|
114
83
|
selectedSkillsTitle: '精选 Skills',
|
|
115
84
|
selectedSkillsFilterLabel: '按能力筛选',
|
|
116
85
|
selectedSkillsEmpty: '没有匹配这个标签的 Skill。',
|
|
117
|
-
//
|
|
118
|
-
//
|
|
119
|
-
// 被挡住的原因只活在**开关左侧那枚三角警示图标**上(悬停 title / 读屏 aria-label 同一串):
|
|
120
|
-
// {count} 是计数占位("6/6"),由代码拼,字典不存数字。
|
|
86
|
+
// Cap blocks adding only; existing selections keep working (runtime does not truncate).
|
|
87
|
+
// Reason lives on the warning icon's title / aria-label; {count} is appended in code.
|
|
121
88
|
selectedSkillsCapTooltip: 'Skill已达上限 ({count})。',
|
|
122
89
|
selectedSkillsCopyName: '复制技能名',
|
|
123
90
|
selectedSkillsCopied: '已复制',
|
|
@@ -146,17 +113,17 @@ const en = {
|
|
|
146
113
|
componentsTitle: 'Components',
|
|
147
114
|
componentsHint: 'The component list is drawn by DSH and appears below.',
|
|
148
115
|
fuyaoLabel: 'Fuyao API Key',
|
|
149
|
-
fuyaoHint: 'Tonghuashun Fuyao structured-data API. Takes effect in new
|
|
116
|
+
fuyaoHint: 'Tonghuashun Fuyao structured-data API. Takes effect in new Auto mode sessions.',
|
|
150
117
|
ftshareLabel: 'FTShare API Key',
|
|
151
|
-
ftshareHint: 'Feikon FTShare structured-data API (free tier). Takes effect in new
|
|
118
|
+
ftshareHint: 'Feikon FTShare structured-data API (free tier). Takes effect in new Auto mode sessions.',
|
|
152
119
|
anysearchLabel: 'AnySearch API Key',
|
|
153
|
-
anysearchHint: 'AnySearch web search / extract interface. Takes effect in new
|
|
120
|
+
anysearchHint: 'AnySearch web search / extract interface. Takes effect in new Auto mode sessions.',
|
|
154
121
|
localFetchLabel: 'Allow local web page extraction',
|
|
155
|
-
localFetchHint: 'When on, a failed fetch falls back to local direct fetching (receipt via: local-http). Takes effect in new
|
|
122
|
+
localFetchHint: 'When on, a failed fetch falls back to local direct fetching (receipt via: local-http). Takes effect in new Auto mode sessions.',
|
|
156
123
|
windLabel: 'Wind Alice API Key',
|
|
157
|
-
windHint: 'Wind financial disclosure document retrieval interface. Takes effect in new
|
|
124
|
+
windHint: 'Wind financial disclosure document retrieval interface. Takes effect in new Auto mode sessions.',
|
|
158
125
|
paddleocrLabel: 'PaddleOCR document token',
|
|
159
|
-
paddleocrHint: 'Configure the token to parse PDFs and images automatically. Takes effect in new
|
|
126
|
+
paddleocrHint: 'Configure the token to parse PDFs and images automatically. Takes effect in new Auto mode sessions.',
|
|
160
127
|
selectedSkillsTitle: 'Selected Skills',
|
|
161
128
|
selectedSkillsFilterLabel: 'Filter by capability',
|
|
162
129
|
selectedSkillsEmpty: 'No skills match this tag.',
|
|
@@ -200,36 +167,27 @@ function installStyles() {
|
|
|
200
167
|
style.textContent = `
|
|
201
168
|
.capital-config-card { display: flex; flex-direction: column; gap: 12px; }
|
|
202
169
|
.capital-config-title { margin: 0; font-size: 14px; font-weight: 500; line-height: 20px; }
|
|
203
|
-
/*
|
|
204
|
-
display,写在 .capital-config-panel 上就等于 hidden 失效——35 颗开关与密钥输入框全留在
|
|
205
|
-
Tab 序里,控件对自己说谎。两条各管一边,谁日后加 display 也漏不出来。 */
|
|
170
|
+
/* Declare display only when not hidden, or the hidden attribute loses to author display. */
|
|
206
171
|
.capital-config-panel { flex-direction: column; gap: 12px; }
|
|
207
172
|
.capital-config-panel:not([hidden]) { display: flex; }
|
|
208
173
|
.capital-config-panel[hidden] { display: none; }
|
|
209
|
-
/*
|
|
210
|
-
只写正面式、两条各自成规则:任何一条失配(宿主改名、老宿主没这段、无 :has() 的引擎、卡片没被
|
|
211
|
-
whileServed 放行、available 为假、属性值意外)都落在**宿主段落照常可见**那一侧——宁可重复,
|
|
212
|
-
不能把用户的数据藏丢。写成 :not([data-capital-config-tab="components"]) 就是把一个拼错的
|
|
213
|
-
属性值变成事故。回到可见一侧的结构保证:tab 条不在任何面板里,永远收不到 hidden。 */
|
|
174
|
+
/* Host rows section: positive :has rules only, so any mismatch leaves it visible. */
|
|
214
175
|
[data-plugin-detail="${BUNDLE_NAME}"]:has(.capital-config-card[data-capital-config-tab="sources"]) section[data-plugin-rows] { display: none; }
|
|
215
176
|
[data-plugin-detail="${BUNDLE_NAME}"]:has(.capital-config-card[data-capital-config-tab="skills"]) section[data-plugin-rows] { display: none; }
|
|
177
|
+
/* Internal roles must stay declared for delegation; hide their user-selectable cards in Settings. */
|
|
178
|
+
[data-agent-preset-id="capital-data-junior"],
|
|
179
|
+
[data-agent-preset-id="capital-visualization-specialist"] { display: none !important; }
|
|
216
180
|
.capital-config-secret-row { display: flex; flex-direction: column; gap: 2px; }
|
|
217
|
-
/*
|
|
218
|
-
收成 0 1 auto 才让链接紧贴其右;配置徽标拿 order + margin-left:auto 留在行尾。 */
|
|
181
|
+
/* Doc link is portaled into the host head row: shrink its label, keep the badge at the end. */
|
|
219
182
|
.capital-config-secret-row label { flex: 0 1 auto; }
|
|
220
183
|
.capital-config-secret-row label ~ span { order: 1; margin-left: auto; }
|
|
221
184
|
.capital-config-field { display: flex; flex-direction: column; gap: 6px; padding: 12px 0; border-top: 0.5px solid var(--dsw-alias-border-l2); }
|
|
222
|
-
/*
|
|
223
|
-
二十几个标签早就滚出视野,筛选器等于失效,所以它还要 position: sticky 跟着滚。
|
|
224
|
-
padding-top: 12px 把左列第一颗按钮顶到 tab 条下方 24px,与第一格「Fuyao API Key」同一档:
|
|
225
|
-
面板只有一个子元素时 flex gap 不发生作用,不补这一档就是 12px,切格子版面就跳一次。
|
|
226
|
-
⚠️ sticky 的参照是页面滚动容器、top 是它顶端还要让出的高度,两者只能在浏览器里量。 */
|
|
185
|
+
/* Filter column stays sticky while the 35-row list scrolls; padding-top matches the 24px rhythm. */
|
|
227
186
|
.capital-config-selected-layout { display: flex; align-items: flex-start; gap: 16px; padding-top: 12px; }
|
|
228
187
|
.capital-config-selected-filter { position: sticky; top: 12px; flex: 0 0 132px; display: flex; flex-direction: column; align-items: stretch; gap: 3px; }
|
|
229
188
|
.capital-config-selected-filter-button { display: flex; align-items: baseline; justify-content: space-between; gap: 6px; min-height: 26px; padding: 3px 8px; border: 0.5px solid var(--dsw-alias-border-l1); border-radius: 6px; background: transparent; color: var(--dsw-alias-label-secondary); font: inherit; font-size: 12px; line-height: 18px; text-align: left; cursor: pointer; }
|
|
230
189
|
.capital-config-selected-filter-button:hover { background: var(--dsw-alias-interactive-bg-hover); color: var(--dsw-alias-label-primary); }
|
|
231
190
|
.capital-config-selected-filter-button[aria-pressed="true"] { border-color: var(--dsw-alias-brand-primary); background: color-mix(in srgb, var(--dsw-alias-brand-primary) 12%, transparent); color: var(--dsw-alias-label-primary); }
|
|
232
|
-
/* 标签后面那个数字 = "点它会显示几行",按 catalog 现算;不是"已开启几颗"(那是 tab 上 2/35 的口径)。 */
|
|
233
191
|
.capital-config-selected-filter-count { flex: none; color: var(--dsw-alias-label-tertiary); font-variant-numeric: tabular-nums; }
|
|
234
192
|
.capital-config-selected-filter-button[aria-pressed="true"] .capital-config-selected-filter-count { color: inherit; }
|
|
235
193
|
.capital-config-selected-list { flex: 1 1 auto; min-width: 0; display: flex; flex-direction: column; }
|
|
@@ -237,30 +195,22 @@ function installStyles() {
|
|
|
237
195
|
.capital-config-skill-tag { padding: 1px 5px; border-radius: 4px; background: var(--dsw-alias-interactive-bg-hover); color: var(--dsw-alias-label-tertiary); font-size: 11px; line-height: 16px; }
|
|
238
196
|
.capital-config-skill-heading { min-width: 0; display: flex; flex-wrap: wrap; align-items: center; gap: 8px; }
|
|
239
197
|
.capital-config-skill-actions { flex: none; display: inline-flex; align-items: center; gap: 6px; }
|
|
240
|
-
/* 两个动作以前是无边框的 tertiary 灰,落在标题后面像一行说明文字,看不出可点。 */
|
|
241
198
|
.capital-config-skill-action { flex: none; min-height: 22px; padding: 2px 8px; border: 0.5px solid var(--dsw-alias-border-l2); border-radius: 6px; background: color-mix(in srgb, var(--dsw-alias-brand-primary) 10%, transparent); color: var(--dsw-alias-brand-primary); font: inherit; font-size: 11px; line-height: 16px; white-space: nowrap; cursor: pointer; }
|
|
242
199
|
.capital-config-skill-action:hover { border-color: var(--dsw-alias-brand-primary); background: color-mix(in srgb, var(--dsw-alias-brand-primary) 18%, transparent); }
|
|
243
200
|
.capital-config-skill-action:focus-visible { outline: var(--dsw-focus-ring-width, 2px) solid var(--dsw-focus-ring-color, var(--dsw-alias-brand-primary)); outline-offset: 1px; }
|
|
244
201
|
.capital-config-skill-repo { display: inline-flex; align-items: center; gap: 3px; color: var(--dsw-alias-label-tertiary); font-size: 11px; line-height: 16px; text-decoration: none; }
|
|
245
202
|
.capital-config-skill-repo:hover { color: var(--dsw-alias-brand-primary); text-decoration: underline; }
|
|
246
203
|
.capital-config-skill-description { margin: 0; color: var(--dsw-alias-label-secondary); font-size: 12px; line-height: 18px; }
|
|
247
|
-
/*
|
|
248
|
-
它默认只有 380px——装不下一份 markdown,所以这里给宽度。选择器写成 div. 是为了在同权重竞争里
|
|
249
|
-
稳赢官方散列类 .dialog:我们的样式表后注入,但"后注入"是巧合不是契约,别押上去。 */
|
|
204
|
+
/* Official Modal defaults to 380px; the div prefix outranks the hashed .dialog class. */
|
|
250
205
|
div.capital-config-skill-modal { width: min(820px, 100%); max-height: 100%; }
|
|
251
|
-
/*
|
|
206
|
+
/* Scroll the body so the dialog's overflow:hidden cannot clip it. */
|
|
252
207
|
.capital-config-skill-modal-content { min-height: 0; overflow: auto; }
|
|
253
|
-
/*
|
|
254
|
-
标题与关闭按钮因此一直留在原位。 */
|
|
208
|
+
/* Show SKILL.md as source, not rendered markdown. */
|
|
255
209
|
.capital-config-skill-doc { min-height: 0; max-height: 58vh; margin: 0; padding: 12px 14px; overflow: auto; border-radius: 8px; background: var(--dsw-alias-interactive-bg-hover); color: var(--dsw-alias-label-secondary); font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; font-size: 11.5px; line-height: 17px; white-space: pre-wrap; overflow-wrap: anywhere; }
|
|
256
210
|
.capital-config-skill-doc-state { margin: 0; padding: 12px 14px; color: var(--dsw-alias-label-tertiary); font-size: 12px; line-height: 18px; }
|
|
257
|
-
/*
|
|
258
|
-
空的——空面板不报错,只有读屏用户付账。position:absolute ⇒ 它对布局一点贡献都没有。 */
|
|
211
|
+
/* aria-controls target must not be empty; absolute keeps it layout-free. */
|
|
259
212
|
.capital-config-sr-only { position: absolute; width: 1px; height: 1px; overflow: hidden; clip-path: inset(50%); white-space: nowrap; }
|
|
260
|
-
/*
|
|
261
|
-
等距。数是这样来的(浏览器实测,别照抄注释改):面板 0 高时宿主 h4 落在 tab 条下方 44px
|
|
262
|
-
= 卡片到面板的 12 + 两格之间的 32;第一格那一档是 24 ⇒ 要收回 44 − 20 = 24。
|
|
263
|
-
面板 hidden 时 display: none,这条 margin 根本不参与布局,所以只有那一格在屏时才生效。 */
|
|
213
|
+
/* Pull the host h4 up to the same 24px rhythm as the other tabs (measured). */
|
|
264
214
|
.capital-config-panel[data-capital-config-panel="components"] { margin-bottom: -20px; }
|
|
265
215
|
.capital-config-doc-link { display: inline-flex; align-items: center; gap: 3px; color: var(--dsw-alias-label-tertiary); font-size: 12px; line-height: 20px; white-space: nowrap; text-decoration: none; }
|
|
266
216
|
.capital-config-doc-link:hover { color: var(--dsw-alias-brand-primary); text-decoration: underline; }
|
|
@@ -269,14 +219,10 @@ function installStyles() {
|
|
|
269
219
|
.capital-config-switch-text { min-width: 0; flex: 1; display: flex; flex-direction: column; gap: 4px; }
|
|
270
220
|
.capital-config-switch-label { color: var(--dsw-alias-label-primary); font-size: 13px; font-weight: 500; line-height: 20px; }
|
|
271
221
|
.capital-config-hint { margin: 0; color: var(--dsw-alias-label-tertiary); font-size: 12px; line-height: 18px; }
|
|
272
|
-
/*
|
|
273
|
-
var() 判败回继承色 ⇒ light 黑 / dark 白(这枚图标与"保存失败"行实机就是这么错的)。
|
|
274
|
-
warn 色用 --dsw-alias-state-warn-primary(StateDot / Tag 的 warning 同款)。cursor:help 让
|
|
275
|
-
"它有话要说"看得出来;padding 2px + 负 margin 只放大悬停命中区、不占布局(14px 的图标当悬停靶子太小)。 */
|
|
222
|
+
/* --dsw-alias-label-error is undefined in the theme, so state tokens only. */
|
|
276
223
|
.capital-config-skill-cap-icon { display: inline-flex; align-items: center; flex: none; padding: 2px; margin: -2px; color: var(--dsw-alias-state-warn-primary); cursor: help; }
|
|
277
224
|
.capital-config-write-failed { margin: 0; color: var(--dsw-alias-state-error-primary); font-size: 12px; line-height: 18px; }
|
|
278
|
-
/*
|
|
279
|
-
免得标题变长时被换行拆成两截。颜色只走 --dsw-* 变量,明暗两套都跟着宿主。 */
|
|
225
|
+
/* Own inline-flex group so the host's wrapping never splits the two links. */
|
|
280
226
|
.capital-config-plugin-links { flex: none; align-items: center; gap: 2px; display: inline-flex; }
|
|
281
227
|
.capital-config-plugin-link { border-radius: var(--dsw-radius-sm); width: 24px; height: 24px; color: var(--dsw-alias-label-tertiary); justify-content: center; align-items: center; display: inline-flex; text-decoration: none; }
|
|
282
228
|
.capital-config-plugin-link:hover { background: var(--dsw-alias-interactive-bg-hover); color: var(--dsw-alias-label-primary); }
|
|
@@ -287,10 +233,6 @@ function installStyles() {
|
|
|
287
233
|
}
|
|
288
234
|
|
|
289
235
|
class CapitalCardController {
|
|
290
|
-
/**
|
|
291
|
-
* @param {object} scope - `ctx.configForms.get(ENTRY_ID)`:本条目的表单与写队列。
|
|
292
|
-
* @param {object} ctx - 浏览器半边上下文,`remote.credentials` 替引用名回答密钥状态。
|
|
293
|
-
*/
|
|
294
236
|
constructor(scope, ctx) {
|
|
295
237
|
this.scope = scope
|
|
296
238
|
this.ctx = ctx
|
|
@@ -299,14 +241,11 @@ class CapitalCardController {
|
|
|
299
241
|
this.localFetchFailed = false
|
|
300
242
|
this.selectedSkillWriting = new Set()
|
|
301
243
|
this.selectedSkillFailed = new Set()
|
|
302
|
-
//
|
|
303
|
-
// 绝不把"读不到"渲染成 0。rowsRequested 记的是"这张卡片真的上过屏":没上屏就不去要这份
|
|
304
|
-
// 逐个 bundle 读 manifest 的清单,事件来了也只是"该重取了",不是"必须现在取"。
|
|
244
|
+
// Row count is external: null = unknown, never 0; fetch only after the card is shown.
|
|
305
245
|
this.bundleRows = null
|
|
306
246
|
this.rowsRequested = false
|
|
307
247
|
this.rowsInFlight = false
|
|
308
|
-
//
|
|
309
|
-
// 本机回退与选用 Skill 两类开关直接 mutate volatile 路径,不进入密钥草稿流。
|
|
248
|
+
// Keys are write-only via the credentials domain; switches mutate volatile paths directly.
|
|
310
249
|
this.model = new SettingsFormModel(scope, [], CREDENTIAL_FIELDS.map((field) => ({
|
|
311
250
|
field,
|
|
312
251
|
write: (value) => this.writeKey(field, value),
|
|
@@ -335,8 +274,7 @@ class CapitalCardController {
|
|
|
335
274
|
configured: this.status[field].configured,
|
|
336
275
|
writable: this.status[field].writable,
|
|
337
276
|
}])),
|
|
338
|
-
//
|
|
339
|
-
// `!== false` 与主插件消费点 resolveLocalFetchConfig 的默认语义逐字一致(默认开)。
|
|
277
|
+
// Strictly from the entry snapshot; `!== false` matches `resolveLocalFetchConfig`.
|
|
340
278
|
localFetch: {
|
|
341
279
|
on: snapshot.value?.retriever?.localFetch?.enabled !== false,
|
|
342
280
|
writing: this.localFetchWriting,
|
|
@@ -347,12 +285,11 @@ class CapitalCardController {
|
|
|
347
285
|
writing: this.selectedSkillWriting.has(field),
|
|
348
286
|
failed: this.selectedSkillFailed.has(field),
|
|
349
287
|
}])),
|
|
350
|
-
// `count === null`
|
|
288
|
+
// `count === null` means unknown, not zero: the card decides whether to draw it.
|
|
351
289
|
components: { count: this.bundleRows },
|
|
352
290
|
}
|
|
353
291
|
}
|
|
354
292
|
|
|
355
|
-
/** 引用名可以被改配置(`fuyaoCredentialRef` 等)换掉:换了名就当没配过,草稿一并清掉。 */
|
|
356
293
|
refreshRefs() {
|
|
357
294
|
const snapshot = this.scope.getSnapshot()
|
|
358
295
|
for (const field of CREDENTIAL_FIELDS) {
|
|
@@ -369,7 +306,7 @@ class CapitalCardController {
|
|
|
369
306
|
try {
|
|
370
307
|
const response = await this.ctx.remote.credentials.describe(refs)
|
|
371
308
|
if (!response?.ok) return
|
|
372
|
-
//
|
|
309
|
+
// Refs changed while reading: discard this batch.
|
|
373
310
|
if (CREDENTIAL_FIELDS.some((field, index) => refs[index] !== this.status[field].ref)) return
|
|
374
311
|
const values = response.value ?? {}
|
|
375
312
|
for (const field of CREDENTIAL_FIELDS) {
|
|
@@ -384,7 +321,7 @@ class CapitalCardController {
|
|
|
384
321
|
}
|
|
385
322
|
this.store.set(this.projection())
|
|
386
323
|
} catch {
|
|
387
|
-
//
|
|
324
|
+
// The next credential event retries.
|
|
388
325
|
}
|
|
389
326
|
}
|
|
390
327
|
|
|
@@ -392,7 +329,6 @@ class CapitalCardController {
|
|
|
392
329
|
if (CREDENTIAL_FIELDS.some((field) => ref === this.status[field].ref)) this.readCredentials()
|
|
393
330
|
}
|
|
394
331
|
|
|
395
|
-
/** 保存流里写一个密钥:写进 credentials 域,随后按主机是否报"已配置"决定成败。 */
|
|
396
332
|
async writeKey(field, value) {
|
|
397
333
|
try {
|
|
398
334
|
await this.ctx.remote.credentials.set(this.status[field].ref, value.trim())
|
|
@@ -404,14 +340,9 @@ class CapitalCardController {
|
|
|
404
340
|
}
|
|
405
341
|
|
|
406
342
|
/**
|
|
407
|
-
*
|
|
408
|
-
*
|
|
409
|
-
*
|
|
410
|
-
* 不需要草稿仪式;`isVolatilePath` 只放行 schema 里标了 `.volatile()` 的路径。
|
|
411
|
-
* - 嵌套路径必须走 `mutate([{ op: 'set', path: [...] }])`:`set(field)` 只支持顶层字段。
|
|
412
|
-
* - 开关严格由条目快照渲染:写被拒(revision 冲突等)时快照不变、开关不翻转
|
|
413
|
-
* (`mutate` 拒绝不抛错,只做恢复读),因此这里不预判结果,只把"没落上"记成
|
|
414
|
-
* `localFetch.failed`,用 Switch 的 title 说出原因。
|
|
343
|
+
* Immediate entry-config write (not a key): atomic, revision-fenced, no draft;
|
|
344
|
+
* nested paths need `mutate`. The switch renders from the snapshot, so a rejected
|
|
345
|
+
* write leaves it unflipped and sets `failed`.
|
|
415
346
|
*/
|
|
416
347
|
async toggleLocalFetch(value) {
|
|
417
348
|
if (this.localFetchWriting) return
|
|
@@ -423,7 +354,7 @@ class CapitalCardController {
|
|
|
423
354
|
try {
|
|
424
355
|
await this.scope.mutate([{ op: 'set', path: ['retriever', 'localFetch', 'enabled'], value }])
|
|
425
356
|
} catch {
|
|
426
|
-
//
|
|
357
|
+
// The landed check below reports the failure.
|
|
427
358
|
}
|
|
428
359
|
this.localFetchWriting = false
|
|
429
360
|
const landed = this.scope.getSnapshot().value?.retriever?.localFetch?.enabled === value
|
|
@@ -450,13 +381,8 @@ class CapitalCardController {
|
|
|
450
381
|
}
|
|
451
382
|
|
|
452
383
|
/**
|
|
453
|
-
*
|
|
454
|
-
*
|
|
455
|
-
*
|
|
456
|
-
* - 只在卡片真的上过屏之后取,且在途合并:这份清单要逐个 bundle 读 manifest,不是廉价读。
|
|
457
|
-
* - 失败(远端抛错、`ok: false`、清单里没有我们这个包)一律不记不缓存 ⇒ 下一次切到那一格会再试
|
|
458
|
-
* (§9.7 ⑤ 的另一半:失败也要留可重试的余地,把一次抢跑失败 memoize 住就等于钉到进程结束)。
|
|
459
|
-
* - 只在数字真的变了才推 store,保住 snapshot 的引用稳定。
|
|
384
|
+
* Count comes only from `pluginManager.listBundles()`. Merge in-flight reads;
|
|
385
|
+
* failures are not cached (the next tab switch retries); push only on change.
|
|
460
386
|
*/
|
|
461
387
|
async ensureBundleRows() {
|
|
462
388
|
this.rowsRequested = true
|
|
@@ -464,7 +390,6 @@ class CapitalCardController {
|
|
|
464
390
|
await this.readBundleRows()
|
|
465
391
|
}
|
|
466
392
|
|
|
467
|
-
/** 变更事件走这条:还没上过屏就不取,上过屏才值得重取。 */
|
|
468
393
|
invalidateBundleRows() {
|
|
469
394
|
if (this.rowsRequested) this.readBundleRows()
|
|
470
395
|
}
|
|
@@ -480,7 +405,7 @@ class CapitalCardController {
|
|
|
480
405
|
: undefined
|
|
481
406
|
if (Array.isArray(bundle?.rows)) count = bundle.rows.length
|
|
482
407
|
} catch {
|
|
483
|
-
//
|
|
408
|
+
// Keep the old value (usually null); retry on the next tab switch or event.
|
|
484
409
|
}
|
|
485
410
|
this.rowsInFlight = false
|
|
486
411
|
if (count === this.bundleRows) return
|
|
@@ -502,12 +427,8 @@ class CapitalCardController {
|
|
|
502
427
|
}
|
|
503
428
|
|
|
504
429
|
/**
|
|
505
|
-
*
|
|
506
|
-
*
|
|
507
|
-
* 链接要落在**标签右边同一行**,而 `SettingsSecretField` 的 `label` 只收字符串(塞不进节点),
|
|
508
|
-
* 所以挂载后找到官方 head 行(`label` 的父节点)把链接 `createPortal` 进去,摆位见 installStyles
|
|
509
|
-
* 那两条选择器。head 行是 React 拥有的节点,portal 由 React 负责插拔,不手工 appendChild。
|
|
510
|
-
* 找不到 head(上游改了 DOM 形状)就退回渲染在本行末尾——位置退化了,但链接不消失。
|
|
430
|
+
* Key row = official `SettingsSecretField` plus a docs link. `label` only takes a
|
|
431
|
+
* string, so the link is portaled into the host head row, falling back inline.
|
|
511
432
|
*/
|
|
512
433
|
function SecretRow({ field, label, hint, docsUrl, state, disabled, onEdit, t }) {
|
|
513
434
|
const rowRef = useRef(null)
|
|
@@ -546,15 +467,9 @@ function skillLocale() {
|
|
|
546
467
|
}
|
|
547
468
|
|
|
548
469
|
/**
|
|
549
|
-
*
|
|
550
|
-
*
|
|
551
|
-
*
|
|
552
|
-
* 下一次悬停还得能重试。
|
|
553
|
-
* - 在途的那颗 Promise 用来合并重复悬停:同一行来回进出不会打两遍同一条本机路由。
|
|
554
|
-
* - 失败要分**两类**说(`error.code` 带出去给行面选文案):回包里有我们那份 `{ error }` ⇒ 路由活着、
|
|
555
|
-
* 是这条 skill 的快照读不到;回包根本不是我们的形状 ⇒ **宿主进程里就没有这条路由**(客户端 bundle
|
|
556
|
-
* 每次打开页面重取,host 半边却要重启才重新 import——升级后正是这个状态)。把后者说成"刷新页面
|
|
557
|
-
* 再试"是指错方向:刷新拿不到一条不存在的路由,只有重启宿主能。
|
|
470
|
+
* On-demand SKILL.md fetch. Cache successes only, merge in-flight requests. Our
|
|
471
|
+
* `{ error }` body means the snapshot is unreadable; a foreign body means the host
|
|
472
|
+
* process has no route (restart, not reload).
|
|
558
473
|
*/
|
|
559
474
|
const skillDocs = new Map()
|
|
560
475
|
const skillDocRequests = new Map()
|
|
@@ -588,7 +503,6 @@ function loadSkillDoc(name) {
|
|
|
588
503
|
return pending
|
|
589
504
|
}
|
|
590
505
|
|
|
591
|
-
/** 「已复制」亮多久:要够读完,又不能亮到让人以为它是个状态开关。 */
|
|
592
506
|
const COPIED_FLASH_MS = 1600
|
|
593
507
|
|
|
594
508
|
function SelectedSkillRow({ entry, state, disabled, capReached = false, capTooltip, onChange, t }) {
|
|
@@ -598,14 +512,13 @@ function SelectedSkillRow({ entry, state, disabled, capReached = false, capToolt
|
|
|
598
512
|
const copyTimer = useRef(null)
|
|
599
513
|
const [copied, setCopied] = useState(false)
|
|
600
514
|
const [detailOpen, setDetailOpen] = useState(false)
|
|
601
|
-
// null = 还没取过;'ready' 之后不再重取,'failed' / 'loading' 允许下一次悬停再来一遍。
|
|
602
515
|
const [doc, setDoc] = useState(null)
|
|
603
516
|
useEffect(() => () => {
|
|
604
517
|
if (copyTimer.current !== null) clearTimeout(copyTimer.current)
|
|
605
518
|
}, [])
|
|
606
519
|
const copyName = async () => {
|
|
607
520
|
const accepted = await writeClipboard(entry.name)
|
|
608
|
-
//
|
|
521
|
+
// Do not flip the label if the clipboard rejected it.
|
|
609
522
|
if (!accepted) return
|
|
610
523
|
setCopied(true)
|
|
611
524
|
if (copyTimer.current !== null) clearTimeout(copyTimer.current)
|
|
@@ -621,7 +534,6 @@ function SelectedSkillRow({ entry, state, disabled, capReached = false, capToolt
|
|
|
621
534
|
loadSkillDoc(entry.name).then(
|
|
622
535
|
(text) => setDoc({ status: 'ready', text }),
|
|
623
536
|
(error) => {
|
|
624
|
-
// 码只留在这里(卡片上那句人话不背 `skill_doc_unreadable` 这种内部名)。
|
|
625
537
|
globalThis.console?.warn?.(`capital-config: SKILL.md 原文读取失败 ${entry.name}: ${String(error?.code ?? error?.message ?? error)}`)
|
|
626
538
|
setDoc({ status: 'failed', code: error?.code === 'route_missing' ? 'route_missing' : 'unreadable' })
|
|
627
539
|
},
|
|
@@ -638,13 +550,10 @@ function SelectedSkillRow({ entry, state, disabled, capReached = false, capToolt
|
|
|
638
550
|
h('div', { className: 'capital-config-skill-heading' },
|
|
639
551
|
h('span', { className: 'capital-config-title' }, label),
|
|
640
552
|
h('div', { className: 'capital-config-skill-actions' },
|
|
641
|
-
//
|
|
642
|
-
// 也用不上,摆在那儿只是 35 行噪音。「详情」不留这一道闸门——开之前正是要读原文的时候。
|
|
553
|
+
// Copy name only on enabled rows; details has no gate.
|
|
643
554
|
state?.on === true ? h('button', {
|
|
644
555
|
type: 'button',
|
|
645
556
|
className: 'capital-config-skill-action',
|
|
646
|
-
// slug 从行面上删掉了,所以无障碍名与原生提示都得带着真名字:用户要能在按下去之前
|
|
647
|
-
// 知道"复制的到底是哪一串",否则删掉那行 `code` 就是把信息藏了而不是省了。
|
|
648
557
|
title: `${t('selectedSkillsCopyName')}: ${entry.name}`,
|
|
649
558
|
'aria-label': `${t('selectedSkillsCopyName')}: ${entry.name}`,
|
|
650
559
|
onClick: copyName,
|
|
@@ -654,9 +563,7 @@ function SelectedSkillRow({ entry, state, disabled, capReached = false, capToolt
|
|
|
654
563
|
className: 'capital-config-skill-action',
|
|
655
564
|
'aria-label': `${t('selectedSkillsDetail')}: ${entry.name}`,
|
|
656
565
|
'aria-haspopup': 'dialog',
|
|
657
|
-
//
|
|
658
|
-
// 点击才是这条动作的正解——官方 HoverCard 只认 pointerenter 计时(500ms),且**点锚点
|
|
659
|
-
// 会把它关掉**,所以"点详情没反应"不是故障而是那个控件的语义。
|
|
566
|
+
// Prefetch on hover / focus so the modal opens with content; click is the action.
|
|
660
567
|
onPointerEnter: prefetchDoc,
|
|
661
568
|
onFocus: prefetchDoc,
|
|
662
569
|
onClick: () => {
|
|
@@ -668,8 +575,6 @@ function SelectedSkillRow({ entry, state, disabled, capReached = false, capToolt
|
|
|
668
575
|
open: detailOpen,
|
|
669
576
|
onClose: () => setDetailOpen(false),
|
|
670
577
|
title: label,
|
|
671
|
-
// slug 从行面上删了,但"我在读哪一份"这个问题在打开的这一刻最该被回答:
|
|
672
|
-
// 对话框的副标题就是它,也是复制要照抄的那一串。
|
|
673
578
|
description: entry.name,
|
|
674
579
|
closeLabel: t('selectedSkillsDetailClose'),
|
|
675
580
|
className: 'capital-config-skill-modal',
|
|
@@ -688,10 +593,8 @@ function SelectedSkillRow({ entry, state, disabled, capReached = false, capToolt
|
|
|
688
593
|
h('div', { className: 'capital-config-skill-tags' }, entry.tags.map((tag) =>
|
|
689
594
|
h('span', { key: tag, className: 'capital-config-skill-tag' }, SELECTED_SKILL_TAG_LABELS[locale][tag] ?? tag))),
|
|
690
595
|
),
|
|
691
|
-
//
|
|
692
|
-
// title /
|
|
693
|
-
// 图标组件不吃自定义 props(artwork 只解构 size/className/strokeWidth),所以 title /
|
|
694
|
-
// aria 挂在包着它的 span 上;`role=img` 让读屏在开关之前先听到"为什么它拨不动"。
|
|
596
|
+
// Capped rows: reason on the icon before the switch; the icon ignores custom
|
|
597
|
+
// props, so title / role sit on its wrapper.
|
|
695
598
|
capReached && state?.on !== true ? h('span', {
|
|
696
599
|
className: 'capital-config-skill-cap-icon',
|
|
697
600
|
title: capTooltip,
|
|
@@ -701,8 +604,7 @@ function SelectedSkillRow({ entry, state, disabled, capReached = false, capToolt
|
|
|
701
604
|
h(Switch, {
|
|
702
605
|
label,
|
|
703
606
|
checked: state?.on === true,
|
|
704
|
-
//
|
|
705
|
-
// 格子里,比不给上限糟得多。被挡住的原因由左边那枚警示图标回答(控件不许对自己的状态说谎)。
|
|
607
|
+
// Cap blocks adding only: an enabled row can always be turned off.
|
|
706
608
|
disabled: disabled || state?.writing === true || (capReached && state?.on !== true),
|
|
707
609
|
onChange,
|
|
708
610
|
}),
|
|
@@ -712,19 +614,10 @@ function SelectedSkillRow({ entry, state, disabled, capReached = false, capToolt
|
|
|
712
614
|
}
|
|
713
615
|
|
|
714
616
|
/**
|
|
715
|
-
*
|
|
716
|
-
*
|
|
717
|
-
*
|
|
718
|
-
*
|
|
719
|
-
* `plugins.bundle.config` 只以 `view: 'page'` 渲染,没有 summary 一说。
|
|
720
|
-
*
|
|
721
|
-
* ⛔ 三块面板**全部挂载**,不在屏的那块用 `hidden` 收起而不是不渲染:`SettingsForm` 卸载即丢弃草稿
|
|
722
|
-
* (填了密钥切一格再回来,密钥就没了)。宿主自己画的「包含的组件」段随 tab 收起/放行,判据是卡片根上
|
|
723
|
-
* 的 `data-capital-config-tab`——所以 `available` 为假时**不画 tab 条、也不写这个属性**:属性缺席时
|
|
724
|
-
* 那两条 `:has()` 认不出卡片,宿主的清单照常可见,我们藏不掉任何用户能看到的东西。
|
|
725
|
-
*
|
|
726
|
-
* 三格的「tab 条 → 首行文字」等距(间距真值见 installStyles 那几条 padding / margin,浏览器里量出来的):
|
|
727
|
-
* 不等距就是每切一格版面跳一次,用户会以为内容被换了。
|
|
617
|
+
* Bundle config page. Inactive panels stay mounted and only `hidden` (unmounting
|
|
618
|
+
* `SettingsForm` discards drafts). The host rows section collapses on
|
|
619
|
+
* `data-capital-config-tab`, so when unavailable we draw neither tabs nor that
|
|
620
|
+
* attribute and the host list stays visible.
|
|
728
621
|
*/
|
|
729
622
|
function CapitalCard(props) {
|
|
730
623
|
const t = props.t
|
|
@@ -733,23 +626,14 @@ function CapitalCard(props) {
|
|
|
733
626
|
const locale = skillLocale()
|
|
734
627
|
const [tab, setTab] = useState('sources')
|
|
735
628
|
const [tagFilter, setTagFilter] = useState('all')
|
|
736
|
-
// tab
|
|
737
|
-
// ⚠️ 所有 hook 必须排在 `if (!state.available) return` **之前**——早退在后面会让两分支的
|
|
738
|
-
// hook 颗数不同,React 直接报 "Rendered fewer hooks than expected"。
|
|
629
|
+
// Fetch on mount: tab labels carry numbers; hooks must precede the unavailable return.
|
|
739
630
|
useEffect(() => { props.ensureBundleRows() }, [])
|
|
740
|
-
//
|
|
741
|
-
// 进入这一格的那一刻定一次**:当场挪行等于把用户手底下那一行抽走,而"我开了哪几颗"是下次进来时
|
|
742
|
-
// 才要抬头看的事。所以这里存的是一刻的名单(一组 key),不是开关的实时状态;tab 上那个
|
|
743
|
-
// `已开启/总数` 仍然实时——数字当场不说谎,位置不必当场动。
|
|
631
|
+
// Pinned order is snapshotted on tab entry so rows never move under the cursor.
|
|
744
632
|
const enabledKeys = () => new Set(SELECTED_SKILL_CATALOG
|
|
745
633
|
.filter((entry) => state.selectedSkills?.[entry.key]?.on === true)
|
|
746
634
|
.map((entry) => entry.key))
|
|
747
635
|
const [pinnedSkills, setPinnedSkills] = useState(enabledKeys)
|
|
748
|
-
// 首帧可能还没读到配置文档(`available` 为假 ⇒ 那一格压根没画),那一刻量不到谁开着;补一次。
|
|
749
636
|
useEffect(() => { if (state.available) setPinnedSkills(enabledKeys()) }, [state.available])
|
|
750
|
-
// 筛选条按"这一档有几行"从高到低排(同数保持 catalog 里的出现顺序,Array#sort 是稳定的):
|
|
751
|
-
// 眼睛先落到的位置留给最常用的那一档。「全部」不参与排序、恒在第一格——它是"不加筛选",
|
|
752
|
-
// 不是与别的标签竞争的一个筛选值。
|
|
753
637
|
const tagCounts = new Map()
|
|
754
638
|
for (const entry of SELECTED_SKILL_CATALOG) {
|
|
755
639
|
for (const tag of entry.tags) tagCounts.set(tag, (tagCounts.get(tag) ?? 0) + 1)
|
|
@@ -795,9 +679,6 @@ function CapitalCard(props) {
|
|
|
795
679
|
),
|
|
796
680
|
state.localFetch?.failed ? h('p', { role: 'status', className: 'capital-config-write-failed' }, t('saveFailed')) : null,
|
|
797
681
|
)
|
|
798
|
-
// 筛选条上的数字 = "点它会显示几行",与右侧清单当场数出来的行数同一口径(都按 catalog 现算)。
|
|
799
|
-
// 「按能力筛选」那句字面提示删了:这一列本来就长在清单左边,形状已经说明它是干什么的,
|
|
800
|
-
// 而名字仍挂在 role=group 的 aria-label 上,读屏用户拿得到。
|
|
801
682
|
const tagLabels = SELECTED_SKILL_TAG_LABELS[locale]
|
|
802
683
|
const skillsFilter = h('div', { className: 'capital-config-selected-filter', role: 'group', 'aria-label': t('selectedSkillsFilterLabel') },
|
|
803
684
|
...tags.map((tag) => h('button', {
|
|
@@ -808,13 +689,12 @@ function CapitalCard(props) {
|
|
|
808
689
|
onClick: () => setTagFilter(tag),
|
|
809
690
|
}, tagLabels[tag] ?? tag, h('span', { className: 'capital-config-selected-filter-count' }, String(tagCounts.get(tag))))),
|
|
810
691
|
)
|
|
811
|
-
//
|
|
812
|
-
// 不挡"已经勾上的生效"——老配置超过上限时运行期照常全部加载,这里只提示收敛。
|
|
692
|
+
// Live count for the tab and cap. The cap is edit-time only: existing selections keep working.
|
|
813
693
|
const enabledSkills = SELECTED_SKILL_CATALOG.filter(
|
|
814
694
|
(entry) => state.selectedSkills?.[entry.key]?.on === true,
|
|
815
695
|
).length
|
|
816
696
|
const capReached = enabledSkills >= SELECTED_SKILLS_MAX_ENABLED
|
|
817
|
-
//
|
|
697
|
+
// Build the shared tooltip once; per-row drift would show different numbers.
|
|
818
698
|
const capTooltip = t('selectedSkillsCapTooltip')
|
|
819
699
|
.replace('{count}', `${enabledSkills}/${SELECTED_SKILLS_MAX_ENABLED}`)
|
|
820
700
|
const skillRows = visibleSkills.length > 0
|
|
@@ -829,12 +709,10 @@ function CapitalCard(props) {
|
|
|
829
709
|
t,
|
|
830
710
|
}))
|
|
831
711
|
: [h('p', { key: 'empty', className: 'capital-config-hint' }, t('selectedSkillsEmpty'))]
|
|
832
|
-
//
|
|
833
|
-
// 这里不画任何汇总提示——一行红字 + 62 个灰开关是两套说法,只留图标一套。
|
|
834
|
-
// 行数取不到就**只留标题**:把"还不知道"画成 `0` 是控件对自己的状态说谎(数字缺席本身就是信号)。
|
|
712
|
+
// No cap banner: the icon is the only explanation. Unknown count prints no number.
|
|
835
713
|
const withCount = (label, count) => (Number.isInteger(count) ? `${label} ${count}` : label)
|
|
836
714
|
const options = [
|
|
837
|
-
//
|
|
715
|
+
// A dirty draft while Save is off-screen must be announced on the tab.
|
|
838
716
|
{ value: 'sources', label: state.dirty ? t('titleUnsaved') : t('title') },
|
|
839
717
|
{ value: 'skills', label: `${t('selectedSkillsTitle')} ${enabledSkills}/${SELECTED_SKILL_CATALOG.length}` },
|
|
840
718
|
{ value: 'components', label: withCount(t('componentsTitle'), state.components?.count) },
|
|
@@ -853,14 +731,12 @@ function CapitalCard(props) {
|
|
|
853
731
|
value: tab,
|
|
854
732
|
options,
|
|
855
733
|
label: t('tabsLabel'),
|
|
856
|
-
// 不锁:一格里的写是逐字段、逐开关的,切格子不会把它们半路丢下。
|
|
857
734
|
disabled: false,
|
|
858
735
|
onChange: (next) => {
|
|
859
736
|
setTab(next)
|
|
860
|
-
//
|
|
737
|
+
// Re-read on every switch to this tab: a previous failure retries here.
|
|
861
738
|
if (next === 'components') props.ensureBundleRows()
|
|
862
|
-
//
|
|
863
|
-
// 它才升到最前(切走的那一下不动,避免把手底下那一行抽走)。
|
|
739
|
+
// Re-pin only when entering Skills, so the row under the cursor never jumps.
|
|
864
740
|
if (next === 'skills') setPinnedSkills(enabledKeys())
|
|
865
741
|
},
|
|
866
742
|
}),
|
|
@@ -871,18 +747,12 @@ function CapitalCard(props) {
|
|
|
871
747
|
h('div', { className: 'capital-config-selected-list' }, ...skillRows),
|
|
872
748
|
),
|
|
873
749
|
]),
|
|
874
|
-
//
|
|
875
|
-
// 用户点这一格看到的就是清单,多余的一行字只是噪音。但 `aria-controls` 指的这块面板不许是空的
|
|
876
|
-
// (空面板不报错,只有读屏用户付账),所以那句话改挂在 sr-only 上:零布局贡献、读得到。
|
|
750
|
+
// The panel is the host's rows list; keep the aria-controls target non-empty via sr-only.
|
|
877
751
|
panel('components', [h('span', { className: 'capital-config-sr-only' }, t('componentsHint'))]),
|
|
878
752
|
)
|
|
879
753
|
}
|
|
880
754
|
|
|
881
|
-
/**
|
|
882
|
-
* 官方 primitives 的图标表里没有 GitHub / npm 字形(整表核过),所以两枚图标自带 path 数据,
|
|
883
|
-
* 取 simple-icons(CC0)。`fill="currentColor"` + 外层 `color: var(--dsw-*)` ⇒ 明暗两套都跟
|
|
884
|
-
* 宿主,功能 CSS 不写主题选择器。
|
|
885
|
-
*/
|
|
755
|
+
/** Official icons have no GitHub / npm glyph; paths come from simple-icons (CC0), currentColor follows the theme. */
|
|
886
756
|
const GLYPHS = {
|
|
887
757
|
github: 'M12 .297c-6.63 0-12 5.373-12 12 0 5.303 3.438 9.8 8.205 11.385.6.113.82-.258.82-.577 0-.285-.01-1.04-.015-2.04-3.338.724-4.042-1.61-4.042-1.61C4.422 18.07 3.633 17.7 3.633 17.7c-1.087-.744.084-.729.084-.729 1.205.084 1.838 1.236 1.838 1.236 1.07 1.835 2.809 1.305 3.495.998.108-.776.417-1.305.76-1.605-2.665-.3-5.466-1.332-5.466-5.93 0-1.31.465-2.38 1.235-3.22-.135-.303-.54-1.523.105-3.176 0 0 1.005-.322 3.3 1.23.96-.267 1.98-.399 3-.405 1.02.006 2.04.138 3 .405 2.28-1.552 3.285-1.23 3.285-1.23.645 1.653.24 2.873.12 3.176.765.84 1.23 1.91 1.23 3.22 0 4.61-2.805 5.625-5.475 5.92.42.36.81 1.096.81 2.22 0 1.606-.015 2.896-.015 3.286 0 .315.21.69.825.57C20.565 22.092 24 17.592 24 12.297c0-6.627-5.373-12-12-12',
|
|
888
758
|
npm: 'M1.763 0C.786 0 0 .786 0 1.763v20.474C0 23.214.786 24 1.763 24h20.474c.977 0 1.763-.786 1.763-1.763V1.763C24 .786 23.214 0 22.237 0zM5.13 5.323l13.837.019-.009 13.836h-3.464l.01-10.382h-3.456L12.04 19.17H5.113z',
|
|
@@ -907,10 +777,7 @@ function PluginLink({ kind, href, label }) {
|
|
|
907
777
|
}, h('path', { d: GLYPHS[kind] })))
|
|
908
778
|
}
|
|
909
779
|
|
|
910
|
-
/**
|
|
911
|
-
* 标题右侧的外链组。同一格座位在**行详情页**与**官方插件页**也会渲染,所以 `subject.kind`
|
|
912
|
-
* 与包名两道闸门都要过;不匹配就 `return null`(list 座位的"我对这个主题没话说")。
|
|
913
|
-
*/
|
|
780
|
+
/** The same slot renders on row detail and plugin pages: gate on subject.kind and package name. */
|
|
914
781
|
function PluginLinks({ subject, t }) {
|
|
915
782
|
if (subject?.kind !== 'bundle' || subject.pkg?.name !== BUNDLE_NAME) return null
|
|
916
783
|
return h('span', { className: 'capital-config-plugin-links' },
|
|
@@ -925,9 +792,7 @@ function apply(ctx) {
|
|
|
925
792
|
const card = new CapitalCardController(ctx.configForms.get(ENTRY_ID), ctx)
|
|
926
793
|
ctx.effect(() => () => card.dispose(), 'capital-config: form subscription')
|
|
927
794
|
ctx.effect(() => ctx.remote.$on('credentials/reference-updated', (ref) => card.refresh(ref)), 'capital-config: credential invalidations')
|
|
928
|
-
//
|
|
929
|
-
// (卡片是模块级单例,缓存的数字活过页面来回)。disposer 必须自己合并成一颗——`ctx.effect`
|
|
930
|
-
// 只认一个返回的清理函数,返回数组等于什么都没清理。
|
|
795
|
+
// `plugin-manager/changed` is authoritative; re-read after reconnect too. Merge disposers into one cleanup.
|
|
931
796
|
ctx.effect(() => {
|
|
932
797
|
const refresh = () => card.invalidateBundleRows()
|
|
933
798
|
const disposers = [
|
|
@@ -938,17 +803,14 @@ function apply(ctx) {
|
|
|
938
803
|
for (const dispose of disposers) dispose()
|
|
939
804
|
}
|
|
940
805
|
}, 'capital-config: bundle row invalidations')
|
|
941
|
-
//
|
|
942
|
-
// 座位是**组合包自己的页面**,key 是包名而非 `<包名>#<行 id>`——条目 id 与槽 key 因此是两个
|
|
943
|
-
// 不同的字符串,各自的同字链见文件头。
|
|
806
|
+
// Register the card only while the host serves this entry; slot key is the bundle name.
|
|
944
807
|
ctx.effect(() => ctx.configForms.whileServed([ENTRY_ID], () => ctx.slots.inject('plugins.bundle.config', () => ctx.slots.register({
|
|
945
808
|
name: 'plugins.bundle.config',
|
|
946
809
|
key: BUNDLE_NAME,
|
|
947
810
|
locale: LOCALE_NS,
|
|
948
811
|
inject: () => card.inject(),
|
|
949
812
|
}, CapitalCard))), 'capital-config: page')
|
|
950
|
-
//
|
|
951
|
-
// `capital-config` 这条配置条目与它无关,所以不进 whileServed。
|
|
813
|
+
// Links are a plugin property, unrelated to whether the config entry is served.
|
|
952
814
|
ctx.effect(() => ctx.slots.inject(BADGE_SLOT, () => ctx.slots.register({
|
|
953
815
|
name: BADGE_SLOT,
|
|
954
816
|
id: `${ENTRY_ID}-plugin-links`,
|