@v587d/capital-generation 0.0.0-stage → 2.6.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/CHANGELOG.md +814 -0
- package/LICENSE +21 -0
- package/README.md +118 -2
- package/assets/cg-icon.png +0 -0
- package/capital-config/client.js +28 -0
- package/capital-config/client.src.cjs +388 -0
- package/capital-config/index.js +113 -0
- package/capital-config/package.json +36 -0
- package/capital-watchlist/client.js +182 -0
- package/capital-watchlist/client.src.cjs +1229 -0
- package/capital-watchlist/index.js +772 -0
- package/capital-watchlist/package.json +37 -0
- package/chart-ui/README.md +112 -0
- package/chart-ui/client.js +16 -0
- package/chart-ui/client.src.cjs +126 -0
- package/chart-ui/index.js +205 -0
- package/chart-ui/package.json +30 -0
- package/cordis.patch.yml +72 -0
- package/lib/agents/bash-guard.d.ts +55 -0
- package/lib/agents/bash-guard.js +162 -0
- package/lib/agents/root-tool-policy.d.ts +164 -0
- package/lib/agents/root-tool-policy.js +309 -0
- package/lib/chart/artifact-ref.d.ts +65 -0
- package/lib/chart/artifact-ref.js +107 -0
- package/lib/chart/downsample.d.ts +8 -0
- package/lib/chart/downsample.js +53 -0
- package/lib/chart/errors.d.ts +16 -0
- package/lib/chart/errors.js +15 -0
- package/lib/chart/events.d.ts +120 -0
- package/lib/chart/events.js +471 -0
- package/lib/chart/html.d.ts +21 -0
- package/lib/chart/html.js +154 -0
- package/lib/chart/runtime.d.ts +20 -0
- package/lib/chart/runtime.js +270 -0
- package/lib/chart/series.d.ts +102 -0
- package/lib/chart/series.js +424 -0
- package/lib/chart/source-token.d.ts +64 -0
- package/lib/chart/source-token.js +217 -0
- package/lib/chart/spec.d.ts +58 -0
- package/lib/chart/spec.js +265 -0
- package/lib/chart/tool.d.ts +86 -0
- package/lib/chart/tool.js +288 -0
- package/lib/chart/vendor/LICENSE +201 -0
- package/lib/chart/vendor/VERSION.json +8 -0
- package/lib/chart/vendor/lightweight-charts.standalone.production.js +7 -0
- package/lib/data-collector/args.d.ts +38 -0
- package/lib/data-collector/args.js +114 -0
- package/lib/data-collector/dataset-tools.d.ts +3 -0
- package/lib/data-collector/dataset-tools.js +421 -0
- package/lib/data-collector/describe.d.ts +99 -0
- package/lib/data-collector/describe.js +256 -0
- package/lib/data-collector/hub.d.ts +187 -0
- package/lib/data-collector/hub.js +469 -0
- package/lib/data-collector/query-time.d.ts +58 -0
- package/lib/data-collector/query-time.js +101 -0
- package/lib/data-collector/query.d.ts +57 -0
- package/lib/data-collector/query.js +473 -0
- package/lib/data-collector/store.d.ts +499 -0
- package/lib/data-collector/store.js +1486 -0
- package/lib/data-collector/time-axis.d.ts +169 -0
- package/lib/data-collector/time-axis.js +419 -0
- package/lib/data-collector/tools.d.ts +32 -0
- package/lib/data-collector/tools.js +216 -0
- package/lib/index.d.ts +275 -0
- package/lib/index.js +458 -0
- package/lib/net/eastmoney-client.d.ts +104 -0
- package/lib/net/eastmoney-client.js +118 -0
- package/lib/net/throttle.d.ts +16 -0
- package/lib/net/throttle.js +34 -0
- package/lib/ocr/client.d.ts +82 -0
- package/lib/ocr/client.js +281 -0
- package/lib/ocr/markdown.d.ts +75 -0
- package/lib/ocr/markdown.js +209 -0
- package/lib/ocr/store.d.ts +119 -0
- package/lib/ocr/store.js +226 -0
- package/lib/sources/eastmoney-http.d.ts +2 -0
- package/lib/sources/eastmoney-http.js +1253 -0
- package/lib/sources/fuyao-core.d.ts +52 -0
- package/lib/sources/fuyao-core.js +96 -0
- package/lib/sources/fuyao-rest.d.ts +14 -0
- package/lib/sources/fuyao-rest.js +2057 -0
- package/lib/sources/security-code.d.ts +27 -0
- package/lib/sources/security-code.js +104 -0
- package/lib/sources/security-identity.d.ts +24 -0
- package/lib/sources/security-identity.js +89 -0
- package/lib/sources/tencent-http.d.ts +2 -0
- package/lib/sources/tencent-http.js +1053 -0
- package/lib/sources/wind-mcp.d.ts +8 -0
- package/lib/sources/wind-mcp.js +593 -0
- package/lib/time/tools.d.ts +74 -0
- package/lib/time/tools.js +464 -0
- package/lib/tool-exec.d.ts +29 -0
- package/lib/tool-exec.js +17 -0
- package/lib/web-retriever/engines.d.ts +61 -0
- package/lib/web-retriever/engines.js +202 -0
- package/lib/web-retriever/html-markdown.d.ts +50 -0
- package/lib/web-retriever/html-markdown.js +321 -0
- package/lib/web-retriever/local-fetch.d.ts +121 -0
- package/lib/web-retriever/local-fetch.js +562 -0
- package/lib/web-retriever/retriever.d.ts +58 -0
- package/lib/web-retriever/retriever.js +94 -0
- package/lib/web-retriever/sources.d.ts +235 -0
- package/lib/web-retriever/sources.js +961 -0
- package/lib/web-retriever/tools.d.ts +50 -0
- package/lib/web-retriever/tools.js +1029 -0
- package/lib/web-retriever/wind-client.d.ts +69 -0
- package/lib/web-retriever/wind-client.js +311 -0
- package/locale/en.json +6 -0
- package/locale/zh.json +6 -0
- package/package.json +84 -4
- package/preset/capital-generation/README.md +161 -0
- package/preset/capital-generation/agent.patch.yml +509 -0
- package/preset/capital-generation/skills/capital-chart-protocol/SKILL.md +137 -0
- package/preset/capital-generation/skills/capital-data-protocol/SKILL.md +416 -0
- package/preset/capital-generation/skills/capital-orchestration/SKILL.md +174 -0
- package/preset/capital-generation/skills/capital-visualization-protocol/SKILL.md +296 -0
- package/preset/capital-generation/skills/capital-web-protocol/SKILL.md +240 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2024 v587d
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,3 +1,119 @@
|
|
|
1
|
-
|
|
1
|
+
<p align="center">
|
|
2
|
+
<img src="assets/banner.jpg" alt="Capital Generation Banner" width="100%">
|
|
3
|
+
</p>
|
|
2
4
|
|
|
3
|
-
|
|
5
|
+
<p align="center">
|
|
6
|
+
<a href="https://github.com/v587d/capital-generation"><img src="https://img.shields.io/badge/Market-CN%20%7C%20HK%20%7C%20US-orange" alt="Market·CN+HK+US"></a>
|
|
7
|
+
<a href="https://github.com/v587d/capital-generation"><img src="https://img.shields.io/badge/Financial-Agent-red" alt="Financial Agent"></a>
|
|
8
|
+
</p>
|
|
9
|
+
<p align="center">
|
|
10
|
+
<a href="https://github.com/deepseek-ai"><img src="https://img.shields.io/badge/DeepSeek_Harness-plugin-blue" alt="DSH Plugin"></a>
|
|
11
|
+
<a href="https://github.com/deepseek-ai"><img src="https://img.shields.io/badge/DeepSeek_Harness-web-orange" alt="DSH Web"></a>
|
|
12
|
+
<a href="https://github.com/deepseek-ai"><img src="https://img.shields.io/badge/DeepSeek_Harness-desktop-brightgreen" alt="DSH Desktop · 已实机验证"></a>
|
|
13
|
+
<a href="https://github.com/deepseek-ai"><img src="https://img.shields.io/badge/DSH%20Baseline-0.2.0--rc.2-blue" alt="DSH@0.2.0-rc.2"></a>
|
|
14
|
+
</p>
|
|
15
|
+
<p align="center">
|
|
16
|
+
<a href="https://awesome-dsh-plugin.com"><img src="https://awesome-dsh-plugin.com/badge.svg" alt="awesome · DSH plugin"></a>
|
|
17
|
+
<a href="LICENSE"><img src="https://img.shields.io/badge/License-MIT-green" alt="License"></a>
|
|
18
|
+
<a href="https://github.com/v587d/capital-generation/releases"><img src="https://img.shields.io/badge/version-2.6.1-9cf" alt="Version"></a>
|
|
19
|
+
</p>
|
|
20
|
+
|
|
21
|
+
# Capital Generation
|
|
22
|
+
|
|
23
|
+
> Next-Gen AI-Driven Capital Generation.
|
|
24
|
+
|
|
25
|
+
Capital Generation 是运行在 [DeepSeek Harness(DSH)](https://github.com/deepseek-ai/deepseek-harness) 里的证券研究插件,面向关注 A 股(近期开放港美股)的个人用户。安装后,在新会话中选择 **Capital 模式**,就可以用自然语言查行情、财务数据、公告和新闻,整理分析并生成图表。它不是独立应用,需要先安装 DSH。
|
|
26
|
+
|
|
27
|
+
**🎞️视频演示:**[安装、配置与使用](https://www.bilibili.com/video/BV1fzaZ6CE9J/)。
|
|
28
|
+
|
|
29
|
+
例如,你可以问:「复盘 XXXX 最近 20 个交易日的走势,结合公告和财务数据说明主要变化,并画一张量价图。」[查看报告样例](docs/sample/指南针复盘和预测报告.md)。
|
|
30
|
+
|
|
31
|
+
> [!IMPORTANT]
|
|
32
|
+
> 本项目仍在探索中,不提供金融服务或投资建议,不保证数据完整、及时或准确,也不承诺投资回报。请自行核对来源并承担投资风险。
|
|
33
|
+
|
|
34
|
+
## 开始使用
|
|
35
|
+
|
|
36
|
+
> [!NOTE]
|
|
37
|
+
> 适配 DSH`@0.2.0-rc.2`。已在 Linux 的 Web profile 和 Windows 桌面端验证;其他环境尚未充分测试。建议使用 `Deepseek/deepseek-flash` ,思考推理能力为 High ;GPT / Claude 尚未充分测试。
|
|
38
|
+
|
|
39
|
+
1. **安装插件。** Linux / macOS 的 Web 或 CLI profile 在终端执行,下面两条任选一条:
|
|
40
|
+
|
|
41
|
+
```bash
|
|
42
|
+
dsh plugin --profile web add @v587d/capital-generation
|
|
43
|
+
dsh plugin --profile web add github:v587d/capital-generation
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
两条装完的功能一样:前者取 npm 上已发布的版本,后者取本仓库当前的代码。
|
|
47
|
+
安装后重启该 profile。Windows 桌面端在「添加插件」中输入 `https://github.com/v587d/capital-generation`。桌面端自带 DSH 宿主,不需要单独用 npm 安装宿主。本仓库已包含构建产物,普通用户不需要克隆仓库或运行构建命令。
|
|
48
|
+
|
|
49
|
+
2. **选填密钥。** 打开 DSH 「插件」 → **Capital Generation** -> 「配置」。
|
|
50
|
+
|
|
51
|
+
| 密钥 | 用途 |
|
|
52
|
+
| --- | --- |
|
|
53
|
+
| [同花顺 Fuyao](https://fuyao.aicubes.cn/docs/) | 行情、财务数据和自选股报价 |
|
|
54
|
+
| [AnySearch](https://www.anysearch.com/docs) | 网页搜索和正文提取 |
|
|
55
|
+
| [Wind Alice](https://market.windalice.com/#/home) | 公告与新闻检索,以及宏观/行业/汇率指标与按日期区间取的历史 K 线 |
|
|
56
|
+
| [PaddleOCR AIStudio](https://aistudio.baidu.com/paddleocr) | 解析 PDF 和图片中的文字 |
|
|
57
|
+
|
|
58
|
+
这些都是第三方服务的密钥,需分别向服务方申请;额度和条款以各服务方为准。
|
|
59
|
+
**本插件所有密钥均不影响插件核心功能**,即缺哪个密钥,仅表示对应能力就不可用,其他功能照常。
|
|
60
|
+
保存后**新建 Capital 模式会话**即可生效,无需重启。
|
|
61
|
+
也可以将密钥写入 `~/.dsh/.credentials.yaml`,名称依次为 `FUYAO_API_KEY`、`ANYSEARCH_API_KEY`、`WIND_API_KEY`、`PADDLE_OCR_TOKEN`。
|
|
62
|
+
密钥由 DSH 凭据服务保存,不会显示在会话配置中。
|
|
63
|
+
|
|
64
|
+
<p align="center">
|
|
65
|
+
<a href="assets/DSH@0.2.0rc2插件主页配置.png">
|
|
66
|
+
<img src="assets/DSH@0.2.0rc2插件主页配置.png" alt="插件主页的「配置」段" width="800">
|
|
67
|
+
</a>
|
|
68
|
+
</p>
|
|
69
|
+
|
|
70
|
+
3. **新建会话。** 在 Agent Preset 选择器中选 **Capital**,然后直接描述研究问题。也可以到「设置」→「Agent 预设」把它设为默认模式。
|
|
71
|
+
|
|
72
|
+
<p align="center">
|
|
73
|
+
<a href="assets/mode_selector.png">
|
|
74
|
+
<img src="assets/mode_selector.png" alt="新会话选择 Capital 模式" width="800">
|
|
75
|
+
</a>
|
|
76
|
+
</p>
|
|
77
|
+
|
|
78
|
+
## 能做什么
|
|
79
|
+
|
|
80
|
+
- **查数据和资料:** 覆盖A股、港股、美股,按需获取行情、财务、指数、基金与宏观行业数据,检索公告、新闻和研报。共计 **92** 个数据收集能力和 **14** 个网页检索能力,部分公开数据源无需额外密钥;具体覆盖范围见 [数据收集能力表](docs/data-collector-capabilities.md) 和 [网页检索能力表](docs/web-retriever-capabilities.md)。
|
|
81
|
+
- **数据分析:** 所有 Agents 均不直接接触原始结构数据,Agents 按需读取数据、校验数据,并写脚本挖掘数据背后含义。
|
|
82
|
+
- **可视化:** 将数据交给不同角色处理,再生成可交互图表。图表会作为本轮交付物出现在会话中,可在侧栏查看或离线打开。了解处理流程可看 [Agent 角色说明](docs/agent-roles.md)。
|
|
83
|
+
- **管理自选股:** 在 Capital 会话中输入 `/` 或点击输入框旁的 `+`,从「指令」中打开「自选股」。刷新失败时已有报价仍显示为旧快照,并标明时间;请勿当作实时价格。
|
|
84
|
+
|
|
85
|
+
会话中的图表效果:
|
|
86
|
+
|
|
87
|
+
| 走势折线 | K 线与量价 |
|
|
88
|
+
| :---: | :---: |
|
|
89
|
+
| <a href="assets/chart-line-sidebar.png"><img src="assets/chart-line-sidebar.png" alt="侧栏折线图" width="400"></a> | <a href="assets/chart-candlestick-sidebar.png"><img src="assets/chart-candlestick-sidebar.png" alt="侧栏 K 线图" width="400"></a> |
|
|
90
|
+
|
|
91
|
+
<p align="center">
|
|
92
|
+
<a href="assets/watchlist-popup.png">
|
|
93
|
+
<img src="assets/watchlist-popup.png" alt="自选股面板" width="420">
|
|
94
|
+
</a>
|
|
95
|
+
</p>
|
|
96
|
+
|
|
97
|
+
## 常见问题
|
|
98
|
+
|
|
99
|
+
- **安装后找不到 Capital 模式?** 确认插件出现在「插件」→ 已安装列表;Web / CLI profile 安装后要重启该 profile,并在新会话选择模式。
|
|
100
|
+
- **配置在哪?** 打开「插件」→ 已安装的 **Capital Generation**,页面里的「配置」段就是;不在 DSH 的普通「设置」页面,也不用点进「包含的组件」。保存密钥后新建会话再试。
|
|
101
|
+
- **桌面端升级后仍是旧版?** 桌面端安装时会固定所解析的版本,不会自动升级。卸载后重新安装,再到插件详情查看版本号;如需指定版本,可在地址末尾加 `#v2.6.0`。
|
|
102
|
+
- **网页抓取失败?** 配置卡片中的「允许启动本地提取网页内容」默认开启:AnySearch 提取失败时可尝试本机直连。关闭后,本机直连的具名来源查询也不可用;请检查该开关和网络连接。
|
|
103
|
+
- **Wind 那部分会消耗积分?** 会。公告与新闻检索、宏观 / 行业 / 汇率指标、按日期区间取的历史 K 线都按次消耗你在 Wind 侧的额度;额度与条款以 Wind 为准,插件不做用量封顶。没配 `WIND_API_KEY` 时这几类不可用,其余功能照常。
|
|
104
|
+
|
|
105
|
+
目前 Windows 桌面端有一项[已知宿主差异](CHANGELOG.md):主 Agent 的部分工具限制可能不生效。请特别留意它实际调用了哪些工具;结构化数据的取数限制仍由工具自身执行。
|
|
106
|
+
|
|
107
|
+
## 开发与贡献
|
|
108
|
+
|
|
109
|
+
普通用户不需要执行下面的命令。修改源码时请参考 [贡献指南](CONTRIBUTING.md) 和 [开发约定](AGENTS.md):
|
|
110
|
+
|
|
111
|
+
```bash
|
|
112
|
+
npm install
|
|
113
|
+
npm run build # 构建 lib/ 和浏览器端资源
|
|
114
|
+
npm test # 构建并运行测试
|
|
115
|
+
npm run check:dsh # 检查 DSH 扩展面兼容性
|
|
116
|
+
npm run smoke:boot # 检查插件装配
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
完整版本历史见 [CHANGELOG.md](CHANGELOG.md)。问题与建议可到 [Issues](https://github.com/v587d/capital-generation/issues) 或 [DSH官方 Discussions](https://github.com/deepseek-ai/deepseek-harness/discussions/6947) 交流。本项目采用 [MIT 许可](LICENSE)。
|
|
Binary file
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
// 由 scripts/build-capital-config-client.mjs 生成,请勿手改;源码见 capital-config/client.src.cjs。
|
|
2
|
+
window.__ModuleLoader__.load({
|
|
3
|
+
id: '@v587d/capital-config',
|
|
4
|
+
factory: (require) => {
|
|
5
|
+
var module = { exports: {} }
|
|
6
|
+
var exports = module.exports
|
|
7
|
+
Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' })
|
|
8
|
+
"use strict";var{createElement:i,useEffect:x,useRef:C,useState:A}=require("react"),{createPortal:L}=require("react-dom"),{SettingsForm:S,SettingsSecretField:R,SettingsFormModel:k,Switch:D,IconRightUpOutlineRegular:_}=require("@deepseek-ai/dsh-client-ui-primitives"),v="capital-config",N="@v587d/capital-generation",F="settings.capital",O="FUYAO_API_KEY",T="ANYSEARCH_API_KEY",H="WIND_API_KEY",P="PADDLE_OCR_TOKEN",f={fuyao:"https://fuyao.aicubes.cn/docs/",anysearch:"https://www.anysearch.com/docs/auth",wind:"https://market.windalice.com/",paddleocr:"https://aistudio.baidu.com/paddleocr"},o=["fuyao","anysearch","wind","paddleocr"],r={fuyao:O,anysearch:T,wind:H,paddleocr:P},I=()=>Object.fromEntries(o.map(a=>[a,{ref:r[a],configured:!1,writable:!0}])),U={title:"\u914D\u7F6E",fuyaoLabel:"Fuyao API Key",fuyaoHint:"\u540C\u82B1\u987A Fuyao \u7ED3\u6784\u5316\u6570\u636E\u63A5\u53E3\u3002\u4FDD\u5B58\u540E\u65B0 Capital \u6A21\u5F0F\u4F1A\u8BDD\u751F\u6548\u3002",anysearchLabel:"AnySearch API Key",anysearchHint:"AnySearch \u7F51\u9875\u641C\u7D22 / \u63D0\u53D6\u63A5\u53E3\u3002\u4FDD\u5B58\u540E\u65B0 Capital \u6A21\u5F0F\u4F1A\u8BDD\u751F\u6548\u3002",localFetchLabel:"\u5141\u8BB8\u542F\u52A8\u672C\u5730\u63D0\u53D6\u7F51\u9875\u5185\u5BB9",localFetchHint:"\u5F00\u542F\u540E\u672C\u5730\u6293\u53D6\u515C\u5E95\uFF08\u56DE\u6267\u6765\u6E90 local-http\uFF09\u3002\u4FDD\u5B58\u540E\u65B0 Capital \u6A21\u5F0F\u4F1A\u8BDD\u751F\u6548\u3002",windLabel:"Wind Alice API Key",windHint:"Wind \u91D1\u878D\u4FE1\u62AB\u7C7B\u6587\u6863\u68C0\u7D22\u63A5\u53E3\u3002\u4FDD\u5B58\u540E\u65B0 Capital \u6A21\u5F0F\u4F1A\u8BDD\u751F\u6548\u3002",paddleocrLabel:"PaddleOCR \u6587\u6863\u89E3\u6790 Token",paddleocrHint:"\u914D\u7F6E Token \u540E\u81EA\u52A8\u89E3\u6790 PDF\u3001\u56FE\u7247\u3002\u4FDD\u5B58\u540E\u65B0 Capital \u6A21\u5F0F\u4F1A\u8BDD\u751F\u6548\u3002",openDocs:"\u5B98\u65B9\u6587\u6863",configured:"\u5DF2\u914D\u7F6E\u5BC6\u94A5\u3002",notConfigured:"\u672A\u914D\u7F6E\u5BC6\u94A5\u3002",save:"\u4FDD\u5B58",saving:"\u4FDD\u5B58\u4E2D\u2026",readOnly:"\u672C\u90E8\u7F72\u7684\u8BBE\u7F6E\u4E3A\u53EA\u8BFB\u3002",saveFailed:"\u672C\u90E8\u7F72\u6CA1\u6709\u63A5\u53D7\u8FD9\u4E9B\u503C\uFF0C\u5DF2\u4FDD\u7559\u4F9B\u4F60\u4FEE\u6539\u3002",unavailable:"\u8BE5\u63D2\u4EF6\u5F53\u524D\u672A\u52A0\u8F7D\uFF0C\u6682\u65F6\u65E0\u6CD5\u914D\u7F6E\u3002"},j={title:"Configuration",fuyaoLabel:"Fuyao API Key",fuyaoHint:"Tonghuashun Fuyao structured-data API. Takes effect in new Capital mode sessions.",anysearchLabel:"AnySearch API Key",anysearchHint:"AnySearch web search / extract interface. Takes effect in new Capital mode sessions.",localFetchLabel:"Allow local web page extraction",localFetchHint:"When on, a failed fetch falls back to local direct fetching (receipt via: local-http). Takes effect in new Capital mode sessions.",windLabel:"Wind Alice API Key",windHint:"Wind financial disclosure document retrieval interface. Takes effect in new Capital mode sessions.",paddleocrLabel:"PaddleOCR document token",paddleocrHint:"Configure the token to parse PDFs and images automatically. Takes effect in new Capital mode sessions.",openDocs:"Official docs",configured:"A key is configured.",notConfigured:"No key is configured.",save:"Save",saving:"Saving\u2026",readOnly:"This deployment stores settings read-only.",saveFailed:"The deployment did not accept these values; they were left for you to correct.",unavailable:"This plugin is not loaded, so it cannot be configured right now."};function h(a,e){return typeof a=="string"&&a.length>0?a:e}function K(a,e){return e==="fuyao"?h(a.value?.fuyaoCredentialRef,r.fuyao):e==="wind"?h(a.value?.retriever?.windDocs?.credentialRef,r.wind):e==="paddleocr"?h(a.value?.retriever?.paddleOcr?.credentialRef,r.paddleocr):h(a.value?.retriever?.credentialRef,r.anysearch)}function W(){if(typeof document>"u"||document.querySelector("style[data-capital-config]"))return()=>{};let a=document.createElement("style");return a.dataset.capitalConfig="true",a.textContent=`
|
|
9
|
+
.capital-config-card { display: flex; flex-direction: column; gap: 12px; }
|
|
10
|
+
.capital-config-title { margin: 0; font-size: 14px; font-weight: 500; line-height: 20px; }
|
|
11
|
+
.capital-config-secret-row { display: flex; flex-direction: column; gap: 2px; }
|
|
12
|
+
/* \u6587\u6863\u94FE\u63A5 portal \u8FDB\u5B98\u65B9 head \u884C\u4E4B\u540E\u9760\u8FD9\u4E24\u6761\u6446\u4F4D\uFF1A\u5B98\u65B9 label \u662F flex:1\uFF08\u4F1A\u5403\u6389\u6574\u884C\uFF09\uFF0C
|
|
13
|
+
\u6536\u6210 0 1 auto \u624D\u8BA9\u94FE\u63A5\u7D27\u8D34\u5176\u53F3\uFF1B\u914D\u7F6E\u5FBD\u6807\u62FF order + margin-left:auto \u7559\u5728\u884C\u5C3E\u3002 */
|
|
14
|
+
.capital-config-secret-row label { flex: 0 1 auto; }
|
|
15
|
+
.capital-config-secret-row label ~ span { order: 1; margin-left: auto; }
|
|
16
|
+
.capital-config-field { display: flex; flex-direction: column; gap: 6px; padding: 12px 0; border-top: 0.5px solid var(--dsw-alias-border-l2); }
|
|
17
|
+
.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; }
|
|
18
|
+
.capital-config-doc-link:hover { color: var(--dsw-alias-brand-primary); text-decoration: underline; }
|
|
19
|
+
.capital-config-doc-link:focus-visible { border-radius: 3px; outline: var(--dsw-focus-ring-width, 2px) solid var(--dsw-focus-ring-color, var(--dsw-alias-brand-primary)); outline-offset: 2px; }
|
|
20
|
+
.capital-config-switch-row { align-items: center; gap: 12px; display: flex; }
|
|
21
|
+
.capital-config-switch-text { min-width: 0; flex: 1; display: flex; flex-direction: column; gap: 4px; }
|
|
22
|
+
.capital-config-switch-label { color: var(--dsw-alias-label-primary); font-size: 13px; font-weight: 500; line-height: 20px; }
|
|
23
|
+
.capital-config-hint { margin: 0; color: var(--dsw-alias-label-tertiary); font-size: 12px; line-height: 18px; }
|
|
24
|
+
.capital-config-write-failed { margin: 0; color: var(--dsw-alias-label-error); font-size: 12px; line-height: 18px; }
|
|
25
|
+
`,document.head.appendChild(a),()=>a.remove()}var g=class{constructor(e,t){this.scope=e,this.ctx=t,this.status=I(),this.localFetchWriting=!1,this.localFetchFailed=!1,this.model=new k(e,[],o.map(n=>({field:n,write:s=>this.writeKey(n,s)}))),this.store=this.model.bind(()=>this.projection()),this.unsubscribe=e.subscribe(()=>{this.refreshRefs(),this.store.set(this.projection()),this.readCredentials()}),this.refreshRefs(),this.readCredentials()}dispose(){this.unsubscribe?.(),this.model.dispose()}projection(){let e=this.scope.getSnapshot();return{...this.model.shell(),...Object.fromEntries(o.map(t=>[t,{...this.model.field(t),configured:this.status[t].configured,writable:this.status[t].writable}])),localFetch:{on:e.value?.retriever?.localFetch?.enabled!==!1,writing:this.localFetchWriting,failed:this.localFetchFailed}}}refreshRefs(){let e=this.scope.getSnapshot();for(let t of o){let n=K(e,t);this.status[t].ref!==n&&(this.status[t]={ref:n,configured:!1,writable:!0},this.model.edit(t,""))}}async readCredentials(){let e=o.map(t=>this.status[t].ref);try{let t=await this.ctx.remote.credentials.describe(e);if(!t?.ok||o.some((s,l)=>e[l]!==this.status[s].ref))return;let n=t.value??{};for(let s of o){let l=this.status[s],c=n[l.ref];c!==void 0&&(this.status[s]={ref:l.ref,configured:c.configured===!0,writable:c.writable!==!1})}this.store.set(this.projection())}catch{}}refresh(e){o.some(t=>e===this.status[t].ref)&&this.readCredentials()}async writeKey(e,t){try{return await this.ctx.remote.credentials.set(this.status[e].ref,t.trim()),await this.readCredentials(),this.status[e].configured===!0}catch{return!1}}async toggleLocalFetch(e){if(this.localFetchWriting)return;let t=this.scope.getSnapshot();if(t.status!=="ready"||!t.writable)return;this.localFetchWriting=!0,this.localFetchFailed=!1,this.store.set(this.projection());try{await this.scope.mutate([{op:"set",path:["retriever","localFetch","enabled"],value:e}])}catch{}this.localFetchWriting=!1,this.scope.getSnapshot().value?.retriever?.localFetch?.enabled===e||(this.localFetchFailed=!0),this.store.set(this.projection())}inject(){return{hooks:{capitalCard:this.store},...this.model.actions(),toggleLocalFetch:e=>this.toggleLocalFetch(e)}}};function u({field:a,label:e,hint:t,docsUrl:n,state:s,disabled:l,onEdit:c,t:d}){let y=C(null),[w,E]=A(null);x(()=>{let m=y.current,p=m?m.querySelector("label"):null;p&&p.parentElement&&E(p.parentElement)},[]);let b=i("a",{href:n,target:"_blank",rel:"noopener noreferrer","aria-label":`${e} ${d("openDocs")}`,"data-capital-config-doc":"true",className:"capital-config-doc-link"},d("openDocs"),i(_,{size:12}));return i("div",{className:"capital-config-secret-row",key:a,ref:y},i(R,{id:`capital-config-${a}-key`,label:e,hint:t,text:s.text,configured:s.configured,stateLabel:s.configured?d("configured"):d("notConfigured"),disabled:l,onEdit:c}),w?L(b,w):b)}function Y(a){let e=a.t,t=a.useCapitalCard(l=>l),n=!t.writable,s=i(S,{labels:{unavailable:e("unavailable"),readOnly:e("readOnly"),saveFailed:e("saveFailed"),save:e("save"),saving:e("saving")},state:t,onSave:a.save,onDiscard:a.discard},i(u,{field:"fuyao",label:e("fuyaoLabel"),hint:e("fuyaoHint"),docsUrl:f.fuyao,state:t.fuyao,disabled:n||t.saving||!t.fuyao.writable,onEdit:l=>a.edit("fuyao",l),t:e}),i(u,{field:"anysearch",label:e("anysearchLabel"),hint:e("anysearchHint"),docsUrl:f.anysearch,state:t.anysearch,disabled:n||t.saving||!t.anysearch.writable,onEdit:l=>a.edit("anysearch",l),t:e}),i(u,{field:"wind",label:e("windLabel"),hint:e("windHint"),docsUrl:f.wind,state:t.wind,disabled:n||t.saving||!t.wind.writable,onEdit:l=>a.edit("wind",l),t:e}),i(u,{field:"paddleocr",label:e("paddleocrLabel"),hint:e("paddleocrHint"),docsUrl:f.paddleocr,state:t.paddleocr,disabled:n||t.saving||!t.paddleocr.writable,onEdit:l=>a.edit("paddleocr",l),t:e}),i("div",{className:"capital-config-field"},i("div",{className:"capital-config-switch-row"},i("div",{className:"capital-config-switch-text"},i("span",{className:"capital-config-switch-label"},e("localFetchLabel")),i("p",{className:"capital-config-hint"},e("localFetchHint"))),i(D,{label:e("localFetchLabel"),checked:t.localFetch?.on!==!1,disabled:n||t.saving||t.localFetch?.writing===!0,onChange:a.toggleLocalFetch})),t.localFetch?.failed?i("p",{role:"status",className:"capital-config-write-failed"},e("saveFailed")):null));return i("div",{className:"capital-config-card"},i("h4",{className:"capital-config-title"},e("title")),s)}function z(a){a.effect(W,"capital-config: styles"),a.effect(()=>a.locale.register(F,{zh:U,en:j}),"capital-config: dictionaries");let e=new g(a.configForms.get(v),a);a.effect(()=>()=>e.dispose(),"capital-config: form subscription"),a.effect(()=>a.remote.$on("credentials/reference-updated",t=>e.refresh(t)),"capital-config: credential invalidations"),a.effect(()=>a.configForms.whileServed([v],()=>a.slots.inject("plugins.bundle.config",()=>a.slots.register({name:"plugins.bundle.config",key:N,locale:F,inject:()=>e.inject()},Y))),"capital-config: page"),a.logger?.info?.("capital-config: settings card mounted")}exports.name="capital-config";exports.inject=["slots","locale","remote","remote.credentials","configForms"];exports.apply=z;
|
|
26
|
+
return module.exports
|
|
27
|
+
},
|
|
28
|
+
})
|
|
@@ -0,0 +1,388 @@
|
|
|
1
|
+
const { createElement: h, useEffect, useRef, useState } = require('react')
|
|
2
|
+
const { createPortal } = require('react-dom')
|
|
3
|
+
const {
|
|
4
|
+
SettingsForm,
|
|
5
|
+
SettingsSecretField,
|
|
6
|
+
SettingsFormModel,
|
|
7
|
+
Switch,
|
|
8
|
+
IconRightUpOutlineRegular,
|
|
9
|
+
} = require('@deepseek-ai/dsh-client-ui-primitives')
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* 卡片读的是**配置条目 id**,坐的是**组合包自己的页面**——两个不同的字符串,各自钉在不同地方:
|
|
13
|
+
*
|
|
14
|
+
* - `ENTRY_ID` 是 settings 命名空间(0.1.7 起命名空间恒等于 profile 条目 id),四处必须同字:
|
|
15
|
+
* `cordis.patch.yml` 的行 id、`capital-config/index.js` 的 `SETTINGS_ENTRY_ID`、主插件
|
|
16
|
+
* `src/index.ts` 的 `CAPITAL_CONFIG_ENTRY_ID`、这里。
|
|
17
|
+
* - `BUNDLE_NAME` 是槽 `plugins.bundle.config` 的 key:页面按**组合包的包名**取该包的配置段
|
|
18
|
+
* (`renderSlot(..., { entryKey: pkg.name })`),所以它必须逐字等于根 `package.json` 的 `name`。
|
|
19
|
+
*
|
|
20
|
+
* 漂移的表现都不是报错,而是**卡片静默消失**;两处对齐由 `test/capital-config.test.mjs` 钉住。
|
|
21
|
+
*
|
|
22
|
+
* 为什么不用行级槽 `plugins.row.config`(key `<包名>#<行 id>`):那把座位在「包含的组件」里再往
|
|
23
|
+
* 下一层,填密钥要点三下;且同包的 `capital-charts` / `preset-capital-generation` 没有表单,
|
|
24
|
+
* 点进去是死路。配置是「这个插件」的属性,组合包层才是它该坐的层。
|
|
25
|
+
*/
|
|
26
|
+
const ENTRY_ID = 'capital-config'
|
|
27
|
+
const BUNDLE_NAME = '@v587d/capital-generation'
|
|
28
|
+
/** 文案字典的命名空间(与配置条目 id 是两回事,按官方 settings 页的 `settings.*` 惯例)。 */
|
|
29
|
+
const LOCALE_NS = 'settings.capital'
|
|
30
|
+
|
|
31
|
+
const FUYAO_DEFAULT_REF = 'FUYAO_API_KEY'
|
|
32
|
+
const ANYSEARCH_DEFAULT_REF = 'ANYSEARCH_API_KEY'
|
|
33
|
+
const WIND_DEFAULT_REF = 'WIND_API_KEY'
|
|
34
|
+
const PADDLE_OCR_DEFAULT_REF = 'PADDLE_OCR_TOKEN'
|
|
35
|
+
const DOC_URLS = {
|
|
36
|
+
fuyao: 'https://fuyao.aicubes.cn/docs/',
|
|
37
|
+
anysearch: 'https://www.anysearch.com/docs/auth',
|
|
38
|
+
wind: 'https://market.windalice.com/',
|
|
39
|
+
paddleocr: 'https://aistudio.baidu.com/paddleocr',
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* 密钥字段清单(渲染顺序 = 数组顺序)。读状态、寻 ref 名、写草稿、事件刷新都遍历这一份:
|
|
44
|
+
* 加一个 Key 以前要在九处各补一遍,漏掉的那处不会报错,只会让用户填了却不生效。
|
|
45
|
+
*/
|
|
46
|
+
const CREDENTIAL_FIELDS = ['fuyao', 'anysearch', 'wind', 'paddleocr']
|
|
47
|
+
const DEFAULT_REFS = {
|
|
48
|
+
fuyao: FUYAO_DEFAULT_REF,
|
|
49
|
+
anysearch: ANYSEARCH_DEFAULT_REF,
|
|
50
|
+
wind: WIND_DEFAULT_REF,
|
|
51
|
+
paddleocr: PADDLE_OCR_DEFAULT_REF,
|
|
52
|
+
}
|
|
53
|
+
const freshStatus = () => Object.fromEntries(
|
|
54
|
+
CREDENTIAL_FIELDS.map((field) => [field, { ref: DEFAULT_REFS[field], configured: false, writable: true }]),
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
const zh = {
|
|
58
|
+
title: '配置',
|
|
59
|
+
fuyaoLabel: 'Fuyao API Key',
|
|
60
|
+
fuyaoHint: '同花顺 Fuyao 结构化数据接口。保存后新 Capital 模式会话生效。',
|
|
61
|
+
anysearchLabel: 'AnySearch API Key',
|
|
62
|
+
anysearchHint: 'AnySearch 网页搜索 / 提取接口。保存后新 Capital 模式会话生效。',
|
|
63
|
+
localFetchLabel: '允许启动本地提取网页内容',
|
|
64
|
+
localFetchHint: '开启后本地抓取兜底(回执来源 local-http)。保存后新 Capital 模式会话生效。',
|
|
65
|
+
windLabel: 'Wind Alice API Key',
|
|
66
|
+
windHint: 'Wind 金融信披类文档检索接口。保存后新 Capital 模式会话生效。',
|
|
67
|
+
paddleocrLabel: 'PaddleOCR 文档解析 Token',
|
|
68
|
+
paddleocrHint: '配置 Token 后自动解析 PDF、图片。保存后新 Capital 模式会话生效。',
|
|
69
|
+
openDocs: '官方文档',
|
|
70
|
+
configured: '已配置密钥。',
|
|
71
|
+
notConfigured: '未配置密钥。',
|
|
72
|
+
save: '保存',
|
|
73
|
+
saving: '保存中…',
|
|
74
|
+
readOnly: '本部署的设置为只读。',
|
|
75
|
+
saveFailed: '本部署没有接受这些值,已保留供你修改。',
|
|
76
|
+
unavailable: '该插件当前未加载,暂时无法配置。',
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const en = {
|
|
80
|
+
title: 'Configuration',
|
|
81
|
+
fuyaoLabel: 'Fuyao API Key',
|
|
82
|
+
fuyaoHint: 'Tonghuashun Fuyao structured-data API. Takes effect in new Capital mode sessions.',
|
|
83
|
+
anysearchLabel: 'AnySearch API Key',
|
|
84
|
+
anysearchHint: 'AnySearch web search / extract interface. Takes effect in new Capital mode sessions.',
|
|
85
|
+
localFetchLabel: 'Allow local web page extraction',
|
|
86
|
+
localFetchHint: 'When on, a failed fetch falls back to local direct fetching (receipt via: local-http). Takes effect in new Capital mode sessions.',
|
|
87
|
+
windLabel: 'Wind Alice API Key',
|
|
88
|
+
windHint: 'Wind financial disclosure document retrieval interface. Takes effect in new Capital mode sessions.',
|
|
89
|
+
paddleocrLabel: 'PaddleOCR document token',
|
|
90
|
+
paddleocrHint: 'Configure the token to parse PDFs and images automatically. Takes effect in new Capital mode sessions.',
|
|
91
|
+
openDocs: 'Official docs',
|
|
92
|
+
configured: 'A key is configured.',
|
|
93
|
+
notConfigured: 'No key is configured.',
|
|
94
|
+
save: 'Save',
|
|
95
|
+
saving: 'Saving…',
|
|
96
|
+
readOnly: 'This deployment stores settings read-only.',
|
|
97
|
+
saveFailed: 'The deployment did not accept these values; they were left for you to correct.',
|
|
98
|
+
unavailable: 'This plugin is not loaded, so it cannot be configured right now.',
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function text(value, fallback) {
|
|
102
|
+
return typeof value === 'string' && value.length > 0 ? value : fallback
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function refOf(snapshot, field) {
|
|
106
|
+
if (field === 'fuyao') return text(snapshot.value?.fuyaoCredentialRef, DEFAULT_REFS.fuyao)
|
|
107
|
+
if (field === 'wind') return text(snapshot.value?.retriever?.windDocs?.credentialRef, DEFAULT_REFS.wind)
|
|
108
|
+
if (field === 'paddleocr') return text(snapshot.value?.retriever?.paddleOcr?.credentialRef, DEFAULT_REFS.paddleocr)
|
|
109
|
+
return text(snapshot.value?.retriever?.credentialRef, DEFAULT_REFS.anysearch)
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function installStyles() {
|
|
113
|
+
if (typeof document === 'undefined' || document.querySelector('style[data-capital-config]')) return () => {}
|
|
114
|
+
const style = document.createElement('style')
|
|
115
|
+
style.dataset.capitalConfig = 'true'
|
|
116
|
+
style.textContent = `
|
|
117
|
+
.capital-config-card { display: flex; flex-direction: column; gap: 12px; }
|
|
118
|
+
.capital-config-title { margin: 0; font-size: 14px; font-weight: 500; line-height: 20px; }
|
|
119
|
+
.capital-config-secret-row { display: flex; flex-direction: column; gap: 2px; }
|
|
120
|
+
/* 文档链接 portal 进官方 head 行之后靠这两条摆位:官方 label 是 flex:1(会吃掉整行),
|
|
121
|
+
收成 0 1 auto 才让链接紧贴其右;配置徽标拿 order + margin-left:auto 留在行尾。 */
|
|
122
|
+
.capital-config-secret-row label { flex: 0 1 auto; }
|
|
123
|
+
.capital-config-secret-row label ~ span { order: 1; margin-left: auto; }
|
|
124
|
+
.capital-config-field { display: flex; flex-direction: column; gap: 6px; padding: 12px 0; border-top: 0.5px solid var(--dsw-alias-border-l2); }
|
|
125
|
+
.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; }
|
|
126
|
+
.capital-config-doc-link:hover { color: var(--dsw-alias-brand-primary); text-decoration: underline; }
|
|
127
|
+
.capital-config-doc-link:focus-visible { border-radius: 3px; outline: var(--dsw-focus-ring-width, 2px) solid var(--dsw-focus-ring-color, var(--dsw-alias-brand-primary)); outline-offset: 2px; }
|
|
128
|
+
.capital-config-switch-row { align-items: center; gap: 12px; display: flex; }
|
|
129
|
+
.capital-config-switch-text { min-width: 0; flex: 1; display: flex; flex-direction: column; gap: 4px; }
|
|
130
|
+
.capital-config-switch-label { color: var(--dsw-alias-label-primary); font-size: 13px; font-weight: 500; line-height: 20px; }
|
|
131
|
+
.capital-config-hint { margin: 0; color: var(--dsw-alias-label-tertiary); font-size: 12px; line-height: 18px; }
|
|
132
|
+
.capital-config-write-failed { margin: 0; color: var(--dsw-alias-label-error); font-size: 12px; line-height: 18px; }
|
|
133
|
+
`
|
|
134
|
+
document.head.appendChild(style)
|
|
135
|
+
return () => style.remove()
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
class CapitalCardController {
|
|
139
|
+
/**
|
|
140
|
+
* @param {object} scope - `ctx.configForms.get(ENTRY_ID)`:本条目的表单与写队列。
|
|
141
|
+
* @param {object} ctx - 浏览器半边上下文,`remote.credentials` 替引用名回答密钥状态。
|
|
142
|
+
*/
|
|
143
|
+
constructor(scope, ctx) {
|
|
144
|
+
this.scope = scope
|
|
145
|
+
this.ctx = ctx
|
|
146
|
+
this.status = freshStatus()
|
|
147
|
+
this.localFetchWriting = false
|
|
148
|
+
this.localFetchFailed = false
|
|
149
|
+
// 四个密钥控件都在配置段之外落盘(write-only,值从不随响应出网),所以 specs 为空:
|
|
150
|
+
// 本卡片不编辑任何普通字段,唯一的配置写入是下面那个即时保存的回退开关。
|
|
151
|
+
this.model = new SettingsFormModel(scope, [], CREDENTIAL_FIELDS.map((field) => ({
|
|
152
|
+
field,
|
|
153
|
+
write: (value) => this.writeKey(field, value),
|
|
154
|
+
})))
|
|
155
|
+
this.store = this.model.bind(() => this.projection())
|
|
156
|
+
this.unsubscribe = scope.subscribe(() => {
|
|
157
|
+
this.refreshRefs()
|
|
158
|
+
this.store.set(this.projection())
|
|
159
|
+
this.readCredentials()
|
|
160
|
+
})
|
|
161
|
+
this.refreshRefs()
|
|
162
|
+
this.readCredentials()
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
dispose() {
|
|
166
|
+
this.unsubscribe?.()
|
|
167
|
+
this.model.dispose()
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
projection() {
|
|
171
|
+
const snapshot = this.scope.getSnapshot()
|
|
172
|
+
return {
|
|
173
|
+
...this.model.shell(),
|
|
174
|
+
...Object.fromEntries(CREDENTIAL_FIELDS.map((field) => [field, {
|
|
175
|
+
...this.model.field(field),
|
|
176
|
+
configured: this.status[field].configured,
|
|
177
|
+
writable: this.status[field].writable,
|
|
178
|
+
}])),
|
|
179
|
+
// 本机直连回退开关:严格渲染自条目配置(schema 解析后带默认值)。
|
|
180
|
+
// `!== false` 与主插件消费点 resolveLocalFetchConfig 的默认语义逐字一致(默认开)。
|
|
181
|
+
localFetch: {
|
|
182
|
+
on: snapshot.value?.retriever?.localFetch?.enabled !== false,
|
|
183
|
+
writing: this.localFetchWriting,
|
|
184
|
+
failed: this.localFetchFailed,
|
|
185
|
+
},
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/** 引用名可以被改配置(`fuyaoCredentialRef` 等)换掉:换了名就当没配过,草稿一并清掉。 */
|
|
190
|
+
refreshRefs() {
|
|
191
|
+
const snapshot = this.scope.getSnapshot()
|
|
192
|
+
for (const field of CREDENTIAL_FIELDS) {
|
|
193
|
+
const next = refOf(snapshot, field)
|
|
194
|
+
if (this.status[field].ref !== next) {
|
|
195
|
+
this.status[field] = { ref: next, configured: false, writable: true }
|
|
196
|
+
this.model.edit(field, '')
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
async readCredentials() {
|
|
202
|
+
const refs = CREDENTIAL_FIELDS.map((field) => this.status[field].ref)
|
|
203
|
+
try {
|
|
204
|
+
const response = await this.ctx.remote.credentials.describe(refs)
|
|
205
|
+
if (!response?.ok) return
|
|
206
|
+
// 读回来后清单已变(配置事件改了 ref 名):这批响应不再对应当前字段,丢掉。
|
|
207
|
+
if (CREDENTIAL_FIELDS.some((field, index) => refs[index] !== this.status[field].ref)) return
|
|
208
|
+
const values = response.value ?? {}
|
|
209
|
+
for (const field of CREDENTIAL_FIELDS) {
|
|
210
|
+
const current = this.status[field]
|
|
211
|
+
const view = values[current.ref]
|
|
212
|
+
if (view === undefined) continue
|
|
213
|
+
this.status[field] = {
|
|
214
|
+
ref: current.ref,
|
|
215
|
+
configured: view.configured === true,
|
|
216
|
+
writable: view.writable !== false,
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
this.store.set(this.projection())
|
|
220
|
+
} catch {
|
|
221
|
+
// 卡片仍可用:下一次凭证事件会重读。
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
refresh(ref) {
|
|
226
|
+
if (CREDENTIAL_FIELDS.some((field) => ref === this.status[field].ref)) this.readCredentials()
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
/** 保存流里写一个密钥:写进 credentials 域,随后按主机是否报"已配置"决定成败。 */
|
|
230
|
+
async writeKey(field, value) {
|
|
231
|
+
try {
|
|
232
|
+
await this.ctx.remote.credentials.set(this.status[field].ref, value.trim())
|
|
233
|
+
await this.readCredentials()
|
|
234
|
+
return this.status[field].configured === true
|
|
235
|
+
} catch {
|
|
236
|
+
return false
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
/**
|
|
241
|
+
* 本机直连回退开关:点即保存,不走上面的 Save 流。
|
|
242
|
+
*
|
|
243
|
+
* - 即时写的是**条目配置**(不是密钥):配置写入是原子的、带 revision 围栏的持久操作,
|
|
244
|
+
* 不需要草稿仪式;`isVolatilePath` 只放行 schema 里标了 `.volatile()` 的路径。
|
|
245
|
+
* - 嵌套路径必须走 `mutate([{ op: 'set', path: [...] }])`:`set(field)` 只支持顶层字段。
|
|
246
|
+
* - 开关严格由条目快照渲染:写被拒(revision 冲突等)时快照不变、开关不翻转
|
|
247
|
+
* (`mutate` 拒绝不抛错,只做恢复读),因此这里不预判结果,只把"没落上"记成
|
|
248
|
+
* `localFetch.failed`,用 Switch 的 title 说出原因。
|
|
249
|
+
*/
|
|
250
|
+
async toggleLocalFetch(value) {
|
|
251
|
+
if (this.localFetchWriting) return
|
|
252
|
+
const snapshot = this.scope.getSnapshot()
|
|
253
|
+
if (snapshot.status !== 'ready' || !snapshot.writable) return
|
|
254
|
+
this.localFetchWriting = true
|
|
255
|
+
this.localFetchFailed = false
|
|
256
|
+
this.store.set(this.projection())
|
|
257
|
+
try {
|
|
258
|
+
await this.scope.mutate([{ op: 'set', path: ['retriever', 'localFetch', 'enabled'], value }])
|
|
259
|
+
} catch {
|
|
260
|
+
// 写请求本身失败:交给下面的落盘校验,不打断流程。
|
|
261
|
+
}
|
|
262
|
+
this.localFetchWriting = false
|
|
263
|
+
const landed = this.scope.getSnapshot().value?.retriever?.localFetch?.enabled === value
|
|
264
|
+
if (!landed) this.localFetchFailed = true
|
|
265
|
+
this.store.set(this.projection())
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
inject() {
|
|
269
|
+
return {
|
|
270
|
+
hooks: {
|
|
271
|
+
capitalCard: this.store,
|
|
272
|
+
},
|
|
273
|
+
...this.model.actions(),
|
|
274
|
+
toggleLocalFetch: (value) => this.toggleLocalFetch(value),
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
/**
|
|
280
|
+
* 密钥行 = 官方 `SettingsSecretField`(它自己管"已配置/未配置"与留空不改写)+ 一个官方文档链接。
|
|
281
|
+
*
|
|
282
|
+
* 链接要落在**标签右边同一行**,而 `SettingsSecretField` 的 `label` 只收字符串(塞不进节点),
|
|
283
|
+
* 所以挂载后找到官方 head 行(`label` 的父节点)把链接 `createPortal` 进去,摆位见 installStyles
|
|
284
|
+
* 那两条选择器。head 行是 React 拥有的节点,portal 由 React 负责插拔,不手工 appendChild。
|
|
285
|
+
* 找不到 head(上游改了 DOM 形状)就退回渲染在本行末尾——位置退化了,但链接不消失。
|
|
286
|
+
*/
|
|
287
|
+
function SecretRow({ field, label, hint, docsUrl, state, disabled, onEdit, t }) {
|
|
288
|
+
const rowRef = useRef(null)
|
|
289
|
+
const [head, setHead] = useState(null)
|
|
290
|
+
useEffect(() => {
|
|
291
|
+
const row = rowRef.current
|
|
292
|
+
const labelEl = row ? row.querySelector('label') : null
|
|
293
|
+
if (labelEl && labelEl.parentElement) setHead(labelEl.parentElement)
|
|
294
|
+
}, [])
|
|
295
|
+
const link = h('a', {
|
|
296
|
+
href: docsUrl,
|
|
297
|
+
target: '_blank',
|
|
298
|
+
rel: 'noopener noreferrer',
|
|
299
|
+
'aria-label': `${label} ${t('openDocs')}`,
|
|
300
|
+
'data-capital-config-doc': 'true',
|
|
301
|
+
className: 'capital-config-doc-link',
|
|
302
|
+
}, t('openDocs'), h(IconRightUpOutlineRegular, { size: 12 }))
|
|
303
|
+
return h('div', { className: 'capital-config-secret-row', key: field, ref: rowRef },
|
|
304
|
+
h(SettingsSecretField, {
|
|
305
|
+
id: `capital-config-${field}-key`,
|
|
306
|
+
label,
|
|
307
|
+
hint,
|
|
308
|
+
text: state.text,
|
|
309
|
+
configured: state.configured,
|
|
310
|
+
stateLabel: state.configured ? t('configured') : t('notConfigured'),
|
|
311
|
+
disabled,
|
|
312
|
+
onEdit,
|
|
313
|
+
}),
|
|
314
|
+
head ? createPortal(link, head) : link,
|
|
315
|
+
)
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
/**
|
|
319
|
+
* 组合包页面的配置段:页面只提供 `<section data-plugin-config>` 的容器与间距,小节标题由卡片
|
|
320
|
+
* 自己画(与页面自绘的「包含的组件」同一字号层级)。`plugins.bundle.config` 只以 `view: 'page'`
|
|
321
|
+
* 渲染,没有 summary 一说。
|
|
322
|
+
*/
|
|
323
|
+
function CapitalCard(props) {
|
|
324
|
+
const t = props.t
|
|
325
|
+
const state = props.useCapitalCard((snapshot) => snapshot)
|
|
326
|
+
const disabled = !state.writable
|
|
327
|
+
const form = h(SettingsForm, {
|
|
328
|
+
labels: {
|
|
329
|
+
unavailable: t('unavailable'),
|
|
330
|
+
readOnly: t('readOnly'),
|
|
331
|
+
saveFailed: t('saveFailed'),
|
|
332
|
+
save: t('save'),
|
|
333
|
+
saving: t('saving'),
|
|
334
|
+
},
|
|
335
|
+
state,
|
|
336
|
+
onSave: props.save,
|
|
337
|
+
onDiscard: props.discard,
|
|
338
|
+
},
|
|
339
|
+
h(SecretRow, { field: 'fuyao', label: t('fuyaoLabel'), hint: t('fuyaoHint'), docsUrl: DOC_URLS.fuyao, state: state.fuyao, disabled: disabled || state.saving || !state.fuyao.writable, onEdit: (value) => props.edit('fuyao', value), t }),
|
|
340
|
+
h(SecretRow, { field: 'anysearch', label: t('anysearchLabel'), hint: t('anysearchHint'), docsUrl: DOC_URLS.anysearch, state: state.anysearch, disabled: disabled || state.saving || !state.anysearch.writable, onEdit: (value) => props.edit('anysearch', value), t }),
|
|
341
|
+
h(SecretRow, { field: 'wind', label: t('windLabel'), hint: t('windHint'), docsUrl: DOC_URLS.wind, state: state.wind, disabled: disabled || state.saving || !state.wind.writable, onEdit: (value) => props.edit('wind', value), t }),
|
|
342
|
+
h(SecretRow, { field: 'paddleocr', label: t('paddleocrLabel'), hint: t('paddleocrHint'), docsUrl: DOC_URLS.paddleocr, state: state.paddleocr, disabled: disabled || state.saving || !state.paddleocr.writable, onEdit: (value) => props.edit('paddleocr', value), t }),
|
|
343
|
+
// 回退开关排在四个密钥之后:它不是密钥,夹在密钥行中间会被读成第五个 Key。
|
|
344
|
+
h('div', { className: 'capital-config-field' },
|
|
345
|
+
h('div', { className: 'capital-config-switch-row' },
|
|
346
|
+
h('div', { className: 'capital-config-switch-text' },
|
|
347
|
+
h('span', { className: 'capital-config-switch-label' }, t('localFetchLabel')),
|
|
348
|
+
h('p', { className: 'capital-config-hint' }, t('localFetchHint')),
|
|
349
|
+
),
|
|
350
|
+
h(Switch, {
|
|
351
|
+
label: t('localFetchLabel'),
|
|
352
|
+
checked: state.localFetch?.on !== false,
|
|
353
|
+
disabled: disabled || state.saving || state.localFetch?.writing === true,
|
|
354
|
+
onChange: props.toggleLocalFetch,
|
|
355
|
+
}),
|
|
356
|
+
),
|
|
357
|
+
// 被拒的即时写不留草稿、也不进表单的 failed(那是保存流的),所以自己说一句:
|
|
358
|
+
// 开关没翻转就是事实,但用户需要知道"点了、没落上"而不是以为点错了。
|
|
359
|
+
state.localFetch?.failed ? h('p', { role: 'status', className: 'capital-config-write-failed' }, t('saveFailed')) : null,
|
|
360
|
+
),
|
|
361
|
+
)
|
|
362
|
+
return h('div', { className: 'capital-config-card' },
|
|
363
|
+
h('h4', { className: 'capital-config-title' }, t('title')),
|
|
364
|
+
form,
|
|
365
|
+
)
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
function apply(ctx) {
|
|
369
|
+
ctx.effect(installStyles, 'capital-config: styles')
|
|
370
|
+
ctx.effect(() => ctx.locale.register(LOCALE_NS, { zh, en }), 'capital-config: dictionaries')
|
|
371
|
+
const card = new CapitalCardController(ctx.configForms.get(ENTRY_ID), ctx)
|
|
372
|
+
ctx.effect(() => () => card.dispose(), 'capital-config: form subscription')
|
|
373
|
+
ctx.effect(() => ctx.remote.$on('credentials/reference-updated', (ref) => card.refresh(ref)), 'capital-config: credential invalidations')
|
|
374
|
+
// 只有 Host 真的在服务这个条目时才注册卡片:没装配 capital-config 的部署里不留痕迹。
|
|
375
|
+
// 座位是**组合包自己的页面**,key 是包名而非 `<包名>#<行 id>`——条目 id 与槽 key 因此是两个
|
|
376
|
+
// 不同的字符串,各自的同字链见文件头。
|
|
377
|
+
ctx.effect(() => ctx.configForms.whileServed([ENTRY_ID], () => ctx.slots.inject('plugins.bundle.config', () => ctx.slots.register({
|
|
378
|
+
name: 'plugins.bundle.config',
|
|
379
|
+
key: BUNDLE_NAME,
|
|
380
|
+
locale: LOCALE_NS,
|
|
381
|
+
inject: () => card.inject(),
|
|
382
|
+
}, CapitalCard))), 'capital-config: page')
|
|
383
|
+
ctx.logger?.info?.('capital-config: settings card mounted')
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
exports.name = 'capital-config'
|
|
387
|
+
exports.inject = ['slots', 'locale', 'remote', 'remote.credentials', 'configForms']
|
|
388
|
+
exports.apply = apply
|