gudong-content-cli 0.0.0-stage → 0.1.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 gudong
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,68 @@
1
- # Temporary Holding Version
1
+ # gudong-content-cli
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ 让 AI Agent(ZCode / Codex / Claude / Cursor…)用一条 shell 命令读你的「好内容」收藏与精读数据:
4
+ 收了哪些文章、读到哪了、划了哪些金句、写了什么想法。
5
+
6
+ 不造 AI、不建服务端。数据在你自己的 WebDAV / S3 云盘上(与「好内容」App 的多端云同步共用同一份
7
+ `content-snapshot.json` 快照),CLI 只是那根「线」。**v1 全部命令只读。**
8
+
9
+ 配套 Skill:`skills/gudong-content/SKILL.md`(SkillHub 搜 `gudong-content`)。
10
+
11
+ ## 安装
12
+
13
+ ```bash
14
+ npm install -g gudong-content-cli@latest
15
+ ```
16
+
17
+ ## 配置
18
+
19
+ 在「好内容」App → 我的 → 多端云同步里配好 WebDAV 并测试通过后,把同一套凭据给 CLI
20
+ (App 的「AI Agent 接入」页可以直接生成带真实凭据的一条龙提示词):
21
+
22
+ ```bash
23
+ # WebDAV(坚果云等)
24
+ gudong-content config init \
25
+ --cloud-type webdav \
26
+ --url https://dav.jianguoyun.com/dav \
27
+ --username user@example.com \
28
+ --password 应用密码 \
29
+ --root-dir GdContent \
30
+ --timezone Asia/Shanghai
31
+
32
+ # 验证
33
+ gudong-content config test
34
+ ```
35
+
36
+ - `root-dir` 必须与 App 云盘同步设置里的目录一致(家族 Gd 命名规范,默认 `GdContent`,开发包为 `GdContentDebug`)。
37
+ - 配置保存在 `~/.gudong-content/config.json`(chmod 600),也支持环境变量回退
38
+ (`CONTENT_CLOUD_TYPE`、`CONTENT_WEBDAV_URL`、`CONTENT_S3_ENDPOINT` 等,家族 CLI 同约定加 CONTENT_ 前缀)。
39
+
40
+ ## 命令
41
+
42
+ | 命令 | 用途 |
43
+ |------|------|
44
+ | `articles [--status unread\|reading\|finished] [--starred] [--limit N]` | 文章清单:进度/星标/划线想法计数(收录时间倒序) |
45
+ | `highlights [--article <id\|标题>] [--color ...] [--since/--until] [--limit]` | 划线金句(时间倒序) |
46
+ | `notes [--article ...] [--limit N]` | 想法:划线想法(附原文上下文)+ 文章级批注 |
47
+ | `search --query "关键词"` | 标题、摘要、划线原文、想法内容的全文搜索 |
48
+ | `stats` | 全库统计:状态分布、划线/想法总数、阅读天数、估算时长、积压未读 |
49
+ | `config init/show/test` | 配置管理(show 已脱敏) |
50
+
51
+ 所有命令成功输出 JSON(exit 0),失败 exit 1 并给 stderr 错误。v1 无写命令、无冲突语义。
52
+
53
+ ## 口径
54
+
55
+ 云端只有一个文件:`{rootDir}/content-snapshot.json`,App 是唯一写者、CLI 只读镜像。
56
+ 协议权威源在 App 侧(content-dong 的 `intents/20261005-feature-agent-access.md` 与
57
+ `lib/features/agent/content_snapshot.dart`);TS 类型是它的镜像,遵守 wire 纪律:
58
+ 读端忽略未知字段、全字段带默认值、禁止严格校验。CLI 上线不是协议变更。
59
+
60
+ stats 口径逐字复刻 App `ArticleRepository.getReadingStats`
61
+ (content-dong `lib/features/library/article_repository.dart`):
62
+ `estimated_hours` = finished×0.4 + reading×0.2 + Σ(excerpt×8×progress)/24000(估算值,非计时);
63
+ `days_read` = lastReadAt 去重自然日数;`stale_unread` = 收录超 14 天未读。
64
+
65
+ ## 相关
66
+
67
+ - 先例:`gudong-moushi-cli`(谋事/Todo)、`gudong-xiexia-cli`(写下)
68
+ - 家族接入规范:ReKnowledge/app-standard/agent-access.md
@@ -0,0 +1,5 @@
1
+ import type { CloudProvider } from './types.js';
2
+ import type { ParsedConfig } from '../config.js';
3
+ export declare function createCloudProvider(cfg: ParsedConfig): CloudProvider;
4
+ /** 文章快照文件名(相对 rootDir),与 App 端快照文件名约定一致(content-dong lib/features/agent/content_snapshot.dart)。 */
5
+ export declare const SNAPSHOT_FILE = "content-snapshot.json";
@@ -0,0 +1,27 @@
1
+ import { S3Provider } from './s3.js';
2
+ import { WebdavProvider } from './webdav.js';
3
+ export function createCloudProvider(cfg) {
4
+ if (cfg.cloudType === 's3' && cfg.s3) {
5
+ return new S3Provider({
6
+ endpoint: cfg.s3.endpoint,
7
+ bucket: cfg.s3.bucket,
8
+ accessKey: cfg.s3.accessKey,
9
+ secretKey: cfg.s3.secretKey,
10
+ region: cfg.s3.region,
11
+ forcePathStyle: cfg.s3.forcePathStyle,
12
+ rootDir: cfg.rootDir,
13
+ });
14
+ }
15
+ if (cfg.cloudType === 'webdav' && cfg.webdav) {
16
+ return new WebdavProvider({
17
+ url: cfg.webdav.url,
18
+ username: cfg.webdav.username,
19
+ password: cfg.webdav.password,
20
+ rootDir: cfg.rootDir,
21
+ });
22
+ }
23
+ throw new Error(`unsupported cloud type or missing config: ${cfg.cloudType}`);
24
+ }
25
+ /** 文章快照文件名(相对 rootDir),与 App 端快照文件名约定一致(content-dong lib/features/agent/content_snapshot.dart)。 */
26
+ export const SNAPSHOT_FILE = 'content-snapshot.json';
27
+ //# sourceMappingURL=factory.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"factory.js","sourceRoot":"","sources":["../../src/cloud/factory.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,UAAU,EAAE,MAAM,SAAS,CAAC;AACrC,OAAO,EAAE,cAAc,EAAE,MAAM,aAAa,CAAC;AAG7C,MAAM,UAAU,mBAAmB,CAAC,GAAiB;IACnD,IAAI,GAAG,CAAC,SAAS,KAAK,IAAI,IAAI,GAAG,CAAC,EAAE,EAAE,CAAC;QACrC,OAAO,IAAI,UAAU,CAAC;YACpB,QAAQ,EAAE,GAAG,CAAC,EAAE,CAAC,QAAQ;YACzB,MAAM,EAAE,GAAG,CAAC,EAAE,CAAC,MAAM;YACrB,SAAS,EAAE,GAAG,CAAC,EAAE,CAAC,SAAS;YAC3B,SAAS,EAAE,GAAG,CAAC,EAAE,CAAC,SAAS;YAC3B,MAAM,EAAE,GAAG,CAAC,EAAE,CAAC,MAAM;YACrB,cAAc,EAAE,GAAG,CAAC,EAAE,CAAC,cAAc;YACrC,OAAO,EAAE,GAAG,CAAC,OAAO;SACrB,CAAC,CAAC;IACL,CAAC;IACD,IAAI,GAAG,CAAC,SAAS,KAAK,QAAQ,IAAI,GAAG,CAAC,MAAM,EAAE,CAAC;QAC7C,OAAO,IAAI,cAAc,CAAC;YACxB,GAAG,EAAE,GAAG,CAAC,MAAM,CAAC,GAAG;YACnB,QAAQ,EAAE,GAAG,CAAC,MAAM,CAAC,QAAQ;YAC7B,QAAQ,EAAE,GAAG,CAAC,MAAM,CAAC,QAAQ;YAC7B,OAAO,EAAE,GAAG,CAAC,OAAO;SACrB,CAAC,CAAC;IACL,CAAC;IACD,MAAM,IAAI,KAAK,CAAC,6CAA6C,GAAG,CAAC,SAAS,EAAE,CAAC,CAAC;AAChF,CAAC;AAED,mGAAmG;AACnG,MAAM,CAAC,MAAM,aAAa,GAAG,uBAAuB,CAAC"}
@@ -0,0 +1,29 @@
1
+ import type { CloudProvider, EntryFile } from './types.js';
2
+ export interface S3ProviderConfig {
3
+ endpoint: string;
4
+ bucket: string;
5
+ accessKey: string;
6
+ secretKey: string;
7
+ region: string;
8
+ forcePathStyle: boolean;
9
+ rootDir: string;
10
+ }
11
+ export declare class S3Provider implements CloudProvider {
12
+ readonly type: "s3";
13
+ private readonly client;
14
+ private readonly bucket;
15
+ private readonly rootDir;
16
+ constructor(cfg: S3ProviderConfig);
17
+ private key;
18
+ /**
19
+ * 服务层传相对 rootDir 的 key(entries/x.json),listFiles 返回的是全 key
20
+ * (GdContent/content-snapshot.json)——统一归一为全 key,与 WebDAV resolve() 对齐,
21
+ * 否则写/删会落到桶根丢掉 rootDir 前缀。
22
+ */
23
+ private resolve;
24
+ listFiles(dirPath: string): Promise<EntryFile[]>;
25
+ read(key: string): Promise<Buffer>;
26
+ write(key: string, data: Buffer): Promise<void>;
27
+ delete(key: string): Promise<boolean>;
28
+ ensureDirectory(): Promise<void>;
29
+ }
@@ -0,0 +1,84 @@
1
+ import { S3Client, ListObjectsV2Command, GetObjectCommand, PutObjectCommand, DeleteObjectCommand, } from '@aws-sdk/client-s3';
2
+ const FILE_PATTERN = /\.(json)$/;
3
+ export class S3Provider {
4
+ type = 's3';
5
+ client;
6
+ bucket;
7
+ rootDir;
8
+ constructor(cfg) {
9
+ this.bucket = cfg.bucket;
10
+ this.rootDir = cfg.rootDir;
11
+ this.client = new S3Client({
12
+ endpoint: cfg.endpoint,
13
+ region: cfg.region,
14
+ credentials: {
15
+ accessKeyId: cfg.accessKey,
16
+ secretAccessKey: cfg.secretKey,
17
+ },
18
+ forcePathStyle: cfg.forcePathStyle,
19
+ });
20
+ }
21
+ key(remotePath) {
22
+ return `${this.rootDir}/${remotePath.replace(/^\/+/, '')}`;
23
+ }
24
+ /**
25
+ * 服务层传相对 rootDir 的 key(entries/x.json),listFiles 返回的是全 key
26
+ * (GdContent/content-snapshot.json)——统一归一为全 key,与 WebDAV resolve() 对齐,
27
+ * 否则写/删会落到桶根丢掉 rootDir 前缀。
28
+ */
29
+ resolve(key) {
30
+ const k = key.replace(/^\/+/, '');
31
+ return k.startsWith(`${this.rootDir}/`) ? k : this.key(k);
32
+ }
33
+ async listFiles(dirPath) {
34
+ const prefix = this.key(dirPath).replace(/\/+$/, '') + '/';
35
+ const out = [];
36
+ let token;
37
+ do {
38
+ const res = await this.client.send(new ListObjectsV2Command({
39
+ Bucket: this.bucket,
40
+ Prefix: prefix,
41
+ ContinuationToken: token,
42
+ }));
43
+ for (const obj of res.Contents ?? []) {
44
+ if (!obj.Key || obj.Key.endsWith('/'))
45
+ continue;
46
+ if (!FILE_PATTERN.test(obj.Key))
47
+ continue;
48
+ out.push({
49
+ id: obj.Key.substring(obj.Key.lastIndexOf('/') + 1).replace(/\.json$/, ''),
50
+ key: obj.Key,
51
+ etag: obj.ETag?.replace(/"/g, '') || undefined,
52
+ lastModified: obj.LastModified,
53
+ size: obj.Size,
54
+ });
55
+ }
56
+ token = res.IsTruncated ? res.NextContinuationToken : undefined;
57
+ } while (token);
58
+ return out;
59
+ }
60
+ async read(key) {
61
+ const res = await this.client.send(new GetObjectCommand({ Bucket: this.bucket, Key: this.resolve(key) }));
62
+ if (!res.Body) {
63
+ throw new Error(`empty body for s3://${this.bucket}/${key}`);
64
+ }
65
+ const bytes = await res.Body.transformToByteArray();
66
+ return Buffer.from(bytes);
67
+ }
68
+ async write(key, data) {
69
+ await this.client.send(new PutObjectCommand({
70
+ Bucket: this.bucket,
71
+ Key: this.resolve(key),
72
+ Body: data,
73
+ ContentType: 'application/json',
74
+ }));
75
+ }
76
+ async delete(key) {
77
+ await this.client.send(new DeleteObjectCommand({ Bucket: this.bucket, Key: this.resolve(key) }));
78
+ return true;
79
+ }
80
+ async ensureDirectory() {
81
+ // S3 没有目录对象
82
+ }
83
+ }
84
+ //# sourceMappingURL=s3.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"s3.js","sourceRoot":"","sources":["../../src/cloud/s3.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,QAAQ,EACR,oBAAoB,EACpB,gBAAgB,EAChB,gBAAgB,EAChB,mBAAmB,GACpB,MAAM,oBAAoB,CAAC;AAa5B,MAAM,YAAY,GAAG,WAAW,CAAC;AAEjC,MAAM,OAAO,UAAU;IACZ,IAAI,GAAG,IAAa,CAAC;IACb,MAAM,CAAW;IACjB,MAAM,CAAS;IACf,OAAO,CAAS;IAEjC,YAAY,GAAqB;QAC/B,IAAI,CAAC,MAAM,GAAG,GAAG,CAAC,MAAM,CAAC;QACzB,IAAI,CAAC,OAAO,GAAG,GAAG,CAAC,OAAO,CAAC;QAC3B,IAAI,CAAC,MAAM,GAAG,IAAI,QAAQ,CAAC;YACzB,QAAQ,EAAE,GAAG,CAAC,QAAQ;YACtB,MAAM,EAAE,GAAG,CAAC,MAAM;YAClB,WAAW,EAAE;gBACX,WAAW,EAAE,GAAG,CAAC,SAAS;gBAC1B,eAAe,EAAE,GAAG,CAAC,SAAS;aAC/B;YACD,cAAc,EAAE,GAAG,CAAC,cAAc;SACnC,CAAC,CAAC;IACL,CAAC;IAEO,GAAG,CAAC,UAAkB;QAC5B,OAAO,GAAG,IAAI,CAAC,OAAO,IAAI,UAAU,CAAC,OAAO,CAAC,MAAM,EAAE,EAAE,CAAC,EAAE,CAAC;IAC7D,CAAC;IAED;;;;OAIG;IACK,OAAO,CAAC,GAAW;QACzB,MAAM,CAAC,GAAG,GAAG,CAAC,OAAO,CAAC,MAAM,EAAE,EAAE,CAAC,CAAC;QAClC,OAAO,CAAC,CAAC,UAAU,CAAC,GAAG,IAAI,CAAC,OAAO,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC;IAC5D,CAAC;IAED,KAAK,CAAC,SAAS,CAAC,OAAe;QAC7B,MAAM,MAAM,GAAG,IAAI,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC,OAAO,CAAC,MAAM,EAAE,EAAE,CAAC,GAAG,GAAG,CAAC;QAC3D,MAAM,GAAG,GAAgB,EAAE,CAAC;QAC5B,IAAI,KAAyB,CAAC;QAC9B,GAAG,CAAC;YACF,MAAM,GAAG,GAAG,MAAM,IAAI,CAAC,MAAM,CAAC,IAAI,CAChC,IAAI,oBAAoB,CAAC;gBACvB,MAAM,EAAE,IAAI,CAAC,MAAM;gBACnB,MAAM,EAAE,MAAM;gBACd,iBAAiB,EAAE,KAAK;aACzB,CAAC,CACH,CAAC;YACF,KAAK,MAAM,GAAG,IAAI,GAAG,CAAC,QAAQ,IAAI,EAAE,EAAE,CAAC;gBACrC,IAAI,CAAC,GAAG,CAAC,GAAG,IAAI,GAAG,CAAC,GAAG,CAAC,QAAQ,CAAC,GAAG,CAAC;oBAAE,SAAS;gBAChD,IAAI,CAAC,YAAY,CAAC,IAAI,CAAC,GAAG,CAAC,GAAG,CAAC;oBAAE,SAAS;gBAC1C,GAAG,CAAC,IAAI,CAAC;oBACP,EAAE,EAAE,GAAG,CAAC,GAAG,CAAC,SAAS,CAAC,GAAG,CAAC,GAAG,CAAC,WAAW,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC,CAAC,OAAO,CAAC,SAAS,EAAE,EAAE,CAAC;oBAC1E,GAAG,EAAE,GAAG,CAAC,GAAG;oBACZ,IAAI,EAAE,GAAG,CAAC,IAAI,EAAE,OAAO,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,SAAS;oBAC9C,YAAY,EAAE,GAAG,CAAC,YAAY;oBAC9B,IAAI,EAAE,GAAG,CAAC,IAAI;iBACf,CAAC,CAAC;YACL,CAAC;YACD,KAAK,GAAG,GAAG,CAAC,WAAW,CAAC,CAAC,CAAC,GAAG,CAAC,qBAAqB,CAAC,CAAC,CAAC,SAAS,CAAC;QAClE,CAAC,QAAQ,KAAK,EAAE;QAChB,OAAO,GAAG,CAAC;IACb,CAAC;IAED,KAAK,CAAC,IAAI,CAAC,GAAW;QACpB,MAAM,GAAG,GAAG,MAAM,IAAI,CAAC,MAAM,CAAC,IAAI,CAAC,IAAI,gBAAgB,CAAC,EAAE,MAAM,EAAE,IAAI,CAAC,MAAM,EAAE,GAAG,EAAE,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC,CAAC;QAC1G,IAAI,CAAC,GAAG,CAAC,IAAI,EAAE,CAAC;YACd,MAAM,IAAI,KAAK,CAAC,uBAAuB,IAAI,CAAC,MAAM,IAAI,GAAG,EAAE,CAAC,CAAC;QAC/D,CAAC;QACD,MAAM,KAAK,GAAG,MACZ,GAAG,CAAC,IACL,CAAC,oBAAoB,EAAE,CAAC;QACzB,OAAO,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;IAC5B,CAAC;IAED,KAAK,CAAC,KAAK,CAAC,GAAW,EAAE,IAAY;QACnC,MAAM,IAAI,CAAC,MAAM,CAAC,IAAI,CACpB,IAAI,gBAAgB,CAAC;YACnB,MAAM,EAAE,IAAI,CAAC,MAAM;YACnB,GAAG,EAAE,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC;YACtB,IAAI,EAAE,IAAI;YACV,WAAW,EAAE,kBAAkB;SAChC,CAAC,CACH,CAAC;IACJ,CAAC;IAED,KAAK,CAAC,MAAM,CAAC,GAAW;QACtB,MAAM,IAAI,CAAC,MAAM,CAAC,IAAI,CAAC,IAAI,mBAAmB,CAAC,EAAE,MAAM,EAAE,IAAI,CAAC,MAAM,EAAE,GAAG,EAAE,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC,CAAC;QACjG,OAAO,IAAI,CAAC;IACd,CAAC;IAED,KAAK,CAAC,eAAe;QACnB,YAAY;IACd,CAAC;CACF"}
@@ -0,0 +1,21 @@
1
+ export interface EntryFile {
2
+ id: string;
3
+ /** 完整云端路径(含 rootDir,如 GdContent/content-snapshot.json 或 WebDAV 的绝对路径) */
4
+ key: string;
5
+ etag?: string;
6
+ lastModified?: Date;
7
+ size?: number;
8
+ }
9
+ /**
10
+ * 云端抽象:与 App 端 CloudService 对齐的最小子集。
11
+ * S3 路径 = {rootDir}/{path};WebDAV 路径 = {serverUrl}/{rootDir}/{path}。
12
+ */
13
+ export interface CloudProvider {
14
+ readonly type: 's3' | 'webdav';
15
+ listFiles(dirPath: string): Promise<EntryFile[]>;
16
+ read(key: string): Promise<Buffer>;
17
+ write(key: string, data: Buffer): Promise<void>;
18
+ delete(key: string): Promise<boolean>;
19
+ /** 确保 rootDir 与子目录存在(WebDAV 需要 MKCOL,S3 无目录概念直接成功) */
20
+ ensureDirectory(dirPath: string): Promise<void>;
21
+ }
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=types.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.js","sourceRoot":"","sources":["../../src/cloud/types.ts"],"names":[],"mappings":""}
@@ -0,0 +1,23 @@
1
+ import type { CloudProvider, EntryFile } from './types.js';
2
+ export interface WebdavProviderConfig {
3
+ url: string;
4
+ username: string;
5
+ password: string;
6
+ rootDir: string;
7
+ }
8
+ export declare class WebdavProvider implements CloudProvider {
9
+ readonly type: "webdav";
10
+ private readonly client;
11
+ private readonly rootDir;
12
+ constructor(cfg: WebdavProviderConfig);
13
+ /** WebDAV 远端路径带前导斜杠:/{rootDir}/{path},与 App 端 url() 拼接一致。 */
14
+ private path;
15
+ listFiles(dirPath: string): Promise<EntryFile[]>;
16
+ /** 服务层传相对 rootDir 的 key(content-snapshot.json)或绝对路径(/GdContent/content-snapshot.json),统一归一为绝对路径。 */
17
+ private resolve;
18
+ read(key: string): Promise<Buffer>;
19
+ write(key: string, data: Buffer): Promise<void>;
20
+ delete(key: string): Promise<boolean>;
21
+ ensureDirectory(dirPath: string): Promise<void>;
22
+ private mkcol;
23
+ }
@@ -0,0 +1,96 @@
1
+ import { createClient } from 'webdav';
2
+ const FILE_PATTERN = /\.(json)$/;
3
+ export class WebdavProvider {
4
+ type = 'webdav';
5
+ client;
6
+ rootDir;
7
+ constructor(cfg) {
8
+ this.rootDir = cfg.rootDir.replace(/^\/+|\/+$/g, '');
9
+ this.client = createClient(cfg.url.replace(/\/+$/, ''), {
10
+ username: cfg.username,
11
+ password: cfg.password,
12
+ });
13
+ }
14
+ /** WebDAV 远端路径带前导斜杠:/{rootDir}/{path},与 App 端 url() 拼接一致。 */
15
+ path(remotePath) {
16
+ return `/${this.rootDir}/${remotePath.replace(/^\/+/, '')}`;
17
+ }
18
+ async listFiles(dirPath) {
19
+ const dir = this.path(dirPath).replace(/\/+$/, '');
20
+ const contents = (await this.client.getDirectoryContents(dir, {
21
+ glob: '*.json',
22
+ }));
23
+ const out = [];
24
+ for (const item of contents) {
25
+ if (!FILE_PATTERN.test(item.basename))
26
+ continue;
27
+ out.push({
28
+ id: item.basename.replace(/\.json$/, ''),
29
+ key: item.filename,
30
+ etag: item.etag?.replace(/"/g, '') || undefined,
31
+ lastModified: item.lastmod ? new Date(item.lastmod) : undefined,
32
+ size: item.size,
33
+ });
34
+ }
35
+ return out;
36
+ }
37
+ /** 服务层传相对 rootDir 的 key(content-snapshot.json)或绝对路径(/GdContent/content-snapshot.json),统一归一为绝对路径。 */
38
+ resolve(key) {
39
+ return key.startsWith('/') ? key : this.path(key);
40
+ }
41
+ async read(key) {
42
+ const buf = await this.client.getFileContents(this.resolve(key), { format: 'binary' });
43
+ return Buffer.isBuffer(buf) ? buf : Buffer.from(buf);
44
+ }
45
+ async write(key, data) {
46
+ await this.client.putFileContents(this.resolve(key), data, { overwrite: true });
47
+ }
48
+ async delete(key) {
49
+ try {
50
+ await this.client.deleteFile(this.resolve(key));
51
+ return true;
52
+ }
53
+ catch (e) {
54
+ if (isMissingObjectError(e))
55
+ return true; // 已不存在视为成功
56
+ throw e;
57
+ }
58
+ }
59
+ async ensureDirectory(dirPath) {
60
+ // 对齐 App 端 ensureDirectory:rootDir 与逐级子目录 MKCOL,已存在(405/parent exists)忽略
61
+ let current = `/${this.rootDir}`;
62
+ await this.mkcol(current);
63
+ for (const segment of dirPath.split('/').filter((s) => s.trim() !== '')) {
64
+ current = `${current}/${segment}`;
65
+ await this.mkcol(current);
66
+ }
67
+ }
68
+ async mkcol(path) {
69
+ try {
70
+ await this.client.createDirectory(path);
71
+ }
72
+ catch (e) {
73
+ const msg = e.message ?? '';
74
+ // 已存在(405)或因父目录已存在而报的错都视为成功
75
+ if (!/405|exists|exist/i.test(msg)) {
76
+ try {
77
+ const ok = await this.client.exists(path);
78
+ if (ok)
79
+ return;
80
+ }
81
+ catch {
82
+ /* fallthrough */
83
+ }
84
+ throw e;
85
+ }
86
+ }
87
+ }
88
+ }
89
+ function isMissingObjectError(e) {
90
+ const err = e;
91
+ const status = err.status ?? err.statusCode;
92
+ if (status === 404)
93
+ return true;
94
+ return (err.message ?? '').toLowerCase().includes('not found');
95
+ }
96
+ //# sourceMappingURL=webdav.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"webdav.js","sourceRoot":"","sources":["../../src/cloud/webdav.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,39 @@
1
+ export interface S3Config {
2
+ endpoint: string;
3
+ bucket: string;
4
+ accessKey: string;
5
+ secretKey: string;
6
+ region: string;
7
+ forcePathStyle: boolean;
8
+ }
9
+ export interface WebdavConfig {
10
+ url: string;
11
+ username: string;
12
+ password: string;
13
+ }
14
+ export interface ParsedConfig {
15
+ cloudType: 's3' | 'webdav';
16
+ /** 云端根目录,与「好内容」App 云盘同步设置里的目录一致(默认 GdContent,开发包 GdContentDebug) */
17
+ rootDir: string;
18
+ /** 折算"今天"用的时区,默认 Asia/Shanghai(与 App 端手机时区对应) */
19
+ timezone: string;
20
+ s3?: S3Config;
21
+ webdav?: WebdavConfig;
22
+ }
23
+ interface RawConfig {
24
+ cloud_type?: string;
25
+ root_dir?: string;
26
+ timezone?: string;
27
+ s3?: Partial<S3Config>;
28
+ webdav?: Partial<WebdavConfig>;
29
+ }
30
+ /**
31
+ * Load config: file first (~/.gudong-content/config.json), then env vars as fallback.
32
+ * Env var names follow the family CLI convention with a CONTENT_ prefix.
33
+ */
34
+ export declare function loadConfig(): ParsedConfig;
35
+ export declare function saveConfig(cfg: RawConfig): void;
36
+ export declare function configExists(): boolean;
37
+ export declare function getConfigPath(): string;
38
+ export declare function getConfigDir(): string;
39
+ export {};
package/dist/config.js ADDED
@@ -0,0 +1,93 @@
1
+ import { readFileSync, writeFileSync, existsSync, mkdirSync, chmodSync } from 'node:fs';
2
+ import { join } from 'node:path';
3
+ import { homedir } from 'node:os';
4
+ const CONFIG_DIR = join(homedir(), '.gudong-content');
5
+ const CONFIG_FILE = join(CONFIG_DIR, 'config.json');
6
+ /**
7
+ * Load config: file first (~/.gudong-content/config.json), then env vars as fallback.
8
+ * Env var names follow the family CLI convention with a CONTENT_ prefix.
9
+ */
10
+ export function loadConfig() {
11
+ let raw = {};
12
+ if (existsSync(CONFIG_FILE)) {
13
+ raw = JSON.parse(readFileSync(CONFIG_FILE, 'utf-8'));
14
+ }
15
+ const env = process.env;
16
+ const cloudType = raw.cloud_type ?? env.CONTENT_CLOUD_TYPE;
17
+ if (!cloudType || (cloudType !== 's3' && cloudType !== 'webdav')) {
18
+ fail('CONTENT_CLOUD_TYPE must be "s3" or "webdav". Run `gudong-content config init` first.');
19
+ }
20
+ const rootDir = (raw.root_dir ?? env.CONTENT_ROOT_DIR ?? 'GdContent').replace(/^\/+|\/+$/g, '');
21
+ const timezone = raw.timezone ?? env.CONTENT_TIMEZONE ?? 'Asia/Shanghai';
22
+ if (cloudType === 's3') {
23
+ const s3 = raw.s3 ?? {};
24
+ const endpoint = s3.endpoint ?? env.CONTENT_S3_ENDPOINT;
25
+ const bucket = s3.bucket ?? env.CONTENT_S3_BUCKET;
26
+ const accessKey = s3.accessKey ?? env.CONTENT_S3_ACCESS_KEY;
27
+ const secretKey = s3.secretKey ?? env.CONTENT_S3_SECRET_KEY;
28
+ const region = s3.region ?? env.CONTENT_S3_REGION ?? 'auto';
29
+ const forcePathStyle = s3.forcePathStyle != null
30
+ ? s3.forcePathStyle
31
+ : parseBool(env.CONTENT_S3_FORCE_PATH_STYLE, true);
32
+ if (!endpoint)
33
+ fail('CONTENT_S3_ENDPOINT is required. Run `gudong-content config init` first.');
34
+ if (!bucket)
35
+ fail('CONTENT_S3_BUCKET is required. Run `gudong-content config init` first.');
36
+ if (!accessKey)
37
+ fail('CONTENT_S3_ACCESS_KEY is required. Run `gudong-content config init` first.');
38
+ if (!secretKey)
39
+ fail('CONTENT_S3_SECRET_KEY is required. Run `gudong-content config init` first.');
40
+ return {
41
+ cloudType: 's3',
42
+ rootDir,
43
+ timezone,
44
+ s3: { endpoint: ensureHttps(endpoint), bucket, accessKey, secretKey, region, forcePathStyle },
45
+ };
46
+ }
47
+ const webdav = raw.webdav ?? {};
48
+ const url = webdav.url ?? env.CONTENT_WEBDAV_URL;
49
+ const username = webdav.username ?? env.CONTENT_WEBDAV_USERNAME;
50
+ const password = webdav.password ?? env.CONTENT_WEBDAV_PASSWORD;
51
+ if (!url)
52
+ fail('CONTENT_WEBDAV_URL is required. Run `gudong-content config init` first.');
53
+ if (!username)
54
+ fail('CONTENT_WEBDAV_USERNAME is required. Run `gudong-content config init` first.');
55
+ if (!password)
56
+ fail('CONTENT_WEBDAV_PASSWORD is required. Run `gudong-content config init` first.');
57
+ return {
58
+ cloudType: 'webdav',
59
+ rootDir,
60
+ timezone,
61
+ webdav: { url, username, password },
62
+ };
63
+ }
64
+ export function saveConfig(cfg) {
65
+ if (!existsSync(CONFIG_DIR))
66
+ mkdirSync(CONFIG_DIR, { recursive: true });
67
+ writeFileSync(CONFIG_FILE, JSON.stringify(cfg, null, 2) + '\n', 'utf-8');
68
+ chmodSync(CONFIG_FILE, 0o600);
69
+ }
70
+ export function configExists() {
71
+ return existsSync(CONFIG_FILE);
72
+ }
73
+ export function getConfigPath() {
74
+ return CONFIG_FILE;
75
+ }
76
+ export function getConfigDir() {
77
+ return CONFIG_DIR;
78
+ }
79
+ function ensureHttps(endpoint) {
80
+ if (endpoint.startsWith('http://') || endpoint.startsWith('https://'))
81
+ return endpoint;
82
+ return `https://${endpoint}`;
83
+ }
84
+ function parseBool(v, fallback) {
85
+ if (v == null)
86
+ return fallback;
87
+ return v === 'true' || v === '1';
88
+ }
89
+ function fail(msg) {
90
+ process.stderr.write(`[gudong-content] config error: ${msg}\n`);
91
+ process.exit(1);
92
+ }
93
+ //# sourceMappingURL=config.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"config.js","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"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"}