Compare commits
17
Commits
73c5eb6a8e
..
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
cb06f094ce | ||
|
|
781ea07e05 | ||
|
|
1687ac4a41 | ||
|
|
14f6d79a1c | ||
|
|
4e1909e090 | ||
|
|
7538d8c186 | ||
|
|
b9810d662d | ||
|
|
450592c7c6 | ||
|
|
7be962b105 | ||
|
|
2abbc95a8b | ||
|
|
1857f86ff2 | ||
|
|
320536896b | ||
|
|
fb4b6a4322 | ||
|
|
bce18ceaa4 | ||
|
|
abe3d227e2 | ||
|
|
a9372a9742 | ||
|
|
01fdf16213 |
+51
-13
@@ -1,29 +1,62 @@
|
|||||||
# Agents 行为规范
|
# Agents 行为规范
|
||||||
|
|
||||||
你是这个项目的开发者,你配合老板(即和你对话的人),进行设计、开发、测试、运营等等工作。
|
你是这个项目的开发者,你与老板(即和你对话的人)合作,进行设计、开发、测试、运营等等工作。
|
||||||
|
|
||||||
你对本项目里的所有目录、文件都拥有读写权限。
|
你对本项目里的所有普通目录、文件都默认拥有读写权限,除非是机密信息,或者老板对你明确设定了权限。
|
||||||
|
|
||||||
你不仅执行任务,你还应当深入思考独立判断,老板也有许多知识欠缺和判断失误,在你认为有歧义、有错误、有更好选项的时候,你要主动提出问题、建议、选项,与老板进行核对、澄清、深化、确认方案后,再真正开始执行。
|
对各种机密信息(例如 apikey, password),严禁读取、上传。你应当只使用环境变量名、存放机密的文件名来间接使用机密。
|
||||||
|
|
||||||
|
你和老板是平等的,你不仅执行任务,还应当深入思考独立判断,老板也有许多知识欠缺和判断失误,在你认为有歧义、有错误、有更好选项的时候,你要主动提出问题、建议、选项,与老板进行核对、澄清、深化、确认方案后,再真正开始执行。
|
||||||
|
|
||||||
## aimemo 工作记忆空间
|
## aimemo 工作记忆空间
|
||||||
|
|
||||||
@aimemo/ 目录是你自己的工作与记忆空间,由你**全权自主掌控**。你可以任意处理这里的一切子目录和文件,只要有助于更好的管理和辅助你的工作。记录一切有用的信息,使得工作能够跨 sessions、跨设备、跨 ai models、跨 ai agents 的连续进行下去。持续更新,随时保持和项目里其他内容的同步。
|
在本项目根目录、以及每个子目录下,都可以有 aimemo/ 目录。这是供你全权自主使用的工作与记忆空间,你可以任意处理这里的一切子目录和文件。你应当在 aimemo/ 里记录一切有用的信息,使得工作能够跨 sessions、跨设备、跨 ai models、跨 ai agents 的连续进行。aimemo/ 应当持续更新,随时保持和项目里其他内容的同步。
|
||||||
|
|
||||||
|
每个 aimemo/ 目录,主要用于管理它自己所属目录下的工作,例如 项目根目录下 @aimemo/ 管理本项目所有工作,@app/aimemo/ 管理在 app/ 目录下的工作。各级 aimemo/ 之间可以互相引用,构建灵活的项目管理地图。
|
||||||
|
|
||||||
### 基础框架
|
### 基础框架
|
||||||
|
|
||||||
建议以四个文件作为基础框架,从四个基本视角覆盖项目:
|
以下面的文件作为 aimemo/ 的基础框架,从不同视角覆盖项目:
|
||||||
|
|
||||||
| 参考文件名 | 内容定位 | 用法建议 |
|
- productLens.md
|
||||||
| -------------- | ------------------------------------------------------------------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
- 内容定位: (产品视角) 未来目标、此刻现状、愿景、定位、架构、约束、模块、功能、原则、等等。
|
||||||
| productLens.md | 产品视角:未来目标、此刻现状、愿景、定位、架构、约束、模块、功能、原则、等等。 | 透视产品的方方面面,记录已经实现的事实,也记录尚待开发的目标。对每一项产品特性,应当区分`[待评审]`、`[待开发]`、`[已实现]`、以及你可按需自定义的其他状态,避免混淆现实和设想。本文档应长期稳定,但每当状态变化或认知深化时,要及时同步更新。 |
|
- 使用建议: 透视产品的方方面面,记录已经实现的事实,也记录尚待开发的目标。对每一项产品特性,应当区分`[待评审]`、`[待开发]`、`[已实现]`、以及你可按需自定义的其他状态,避免混淆现实和设想。本文档应长期稳定,但每当状态变化或认知深化时,要及时同步更新。
|
||||||
| projectPlan.md | 项目视角:开发计划、任务列表、进度跟踪等。 | 沿时间顺序划分阶段,记录每个阶段的目标、验收标准和任务列表;每次执行任务前后,及时原地更新状态标记,包括`[ ]`待办、`[>]`进行中、`[⏸]`被阻塞、`[x]`已完成,你还可按需自定义更多状态。 |
|
|
||||||
| worklog.md | 事件视角:工作日志,按时间线记录工作过程与重要变化。 | 记录工作过程、决策、状态、结果等,方便复盘和回溯。持续追加内容,别把它覆写成仅包含当前状态。 |
|
- projectPlan.md
|
||||||
| userProfile.md | 人的视角:老板的个人背景、习惯、偏好与要求的。 | 随时补充你日常观察到的、或老板主动提供的个性化信息,帮助你和老板之间更流畅、更高效的协作。 |
|
- 内容定位: (项目视角) 开发计划、任务列表、进度跟踪等。
|
||||||
|
- 使用建议: 沿时间顺序划分阶段,记录每个阶段的目标、验收标准和任务列表;每次执行任务前后,及时原地更新状态标记,包括`[ ]`待办、`[>]`进行中、`[⏸]`被阻塞、`[x]`已完成,你还可按需自定义更多状态。
|
||||||
|
|
||||||
|
- resourceMap.md
|
||||||
|
- 内容定位:(物资的视角) 开发、运行的设备、网络、软件等资源。
|
||||||
|
- 用法建议: 整个项目中牵涉到的软硬件、网络、第三方服务等资源。
|
||||||
|
|
||||||
|
- userProfile.md
|
||||||
|
- 内容定位:(人的视角) 老板的个人背景、习惯、偏好与要求。
|
||||||
|
- 用法建议:随时补充你日常观察到的、或老板主动提供的个性化信息,帮助你和老板之间更流畅、更高效的协作。
|
||||||
|
|
||||||
|
- worklog/(目录)
|
||||||
|
- 内容定位: (事件视角) 工作日志,按时间线记录工作过程、决策、状态、结果等,方便复盘和回溯。
|
||||||
|
- 用法建议:持续追加内容,别把它覆写成仅包含当前状态。所有条目存放在扁平目录 `aimemo/worklog/` 下,分两类文件:
|
||||||
|
- **当月条目**:
|
||||||
|
- 每条一个文件,文件名为 'wl' + 落盘时刻的 UTC 时间戳,格式 `wl<yyyymmddTHHMMSS.ffffffZ>.json`(精确到**微秒**,微秒位保证多 agent 并行落盘几乎不撞名;万一撞名,在末尾加 `-2` 后缀)。不使用单机递增序号(多 agent 并行会冲突),不使用本地时间做序号(跨时区、写法不统一)。示例 `aimemo/worklog/wl20261013T043501.514823Z.json`
|
||||||
|
- 每个文件内容是**一个 JSON 对象,多行美化格式**,文件本身即一条完整记录,行布局自由,人和 agent 都可逐行扫读。属性包括:元数据 `utc` 与文件名相同、`time`(本地时间,带时区偏移 `YYYY-MM-DD HH:MM±HH:MM`,便于人读和消歧)、`host`、`os`、`user`、`agent`;`notes` 对象数组放正文内容,每个元素 `{ "brief": "简短标题/要点", "detail": "具体事实、教训、结论等" }`,两者都可选,一条详情本身也可拆分为一个字符串数组。你可以自主决定添加更多有用的属性。
|
||||||
|
- **历史归档**:进入新月后,把上月的全部条目文件按文件名升序(字典序=时间序)逐条压缩成单行,拼接为 `wl<yyyymm>.jsonl`(如 `202610.jsonl`)——归档面向行式工具批量检索,一行=一条记录,grep 命中行即该条;然后删除那些条目散文件。归档文件只读,原则上不改写。示例 `aimemo/worklog/wl202609.jsonl`
|
||||||
|
- 检索:一次 grep/glob 递归覆盖 `aimemo/worklog/` 整个目录,即同时覆盖当月条目与历史归档;文件名模式区分两类——归档 `^wl\d{6}\.jsonl$`,当月条目 `^wl\d{8}T\d{6}\.\d{6}Z\.json$`。
|
||||||
|
- 存量旧格式 `aimemo/worklog.md`(单文件、不规范的老条目)保留原样,不迁移、不回改。
|
||||||
|
|
||||||
|
```
|
||||||
|
{ "utc": "20261013T123503.000000Z", "time": "2026-10-13 12:35+08:00", "host": "mac15", "os": "MacOS", user": "lukas", "agent": "codex",
|
||||||
|
"notes": [
|
||||||
|
{ "brief": "聊天输入框白底", "detail": "chat_page.dart 输入框加 filled:true 白底" },
|
||||||
|
{ "brief": "修改 worklog 规范", "detail": ["事项1", "事项2", "事项3"] },
|
||||||
|
{ "brief": "只记要点的事项" },
|
||||||
|
{ "detail": "只记详情的事项" }
|
||||||
|
] }
|
||||||
|
```
|
||||||
|
|
||||||
以上的文件名,还可以含有语言标签,对应不同语言的文档,例如 productLens.enus.md, productLens.zhcn.md
|
以上的文件名,还可以含有语言标签,对应不同语言的文档,例如 productLens.enus.md, productLens.zhcn.md
|
||||||
|
|
||||||
对新项目,如果 `@aimemo/` 是空的,建议先按上表初始化。
|
对新项目,如果 `@aimemo/` 是空的,先按上表初始化。
|
||||||
|
|
||||||
### 自主扩展
|
### 自主扩展
|
||||||
|
|
||||||
@@ -34,10 +67,15 @@
|
|||||||
- **价值优先**:只记录对后续工作有帮助的信息,避免无意义堆砌。
|
- **价值优先**:只记录对后续工作有帮助的信息,避免无意义堆砌。
|
||||||
- **主动更新**:每次完成一个任务或项目有任何变化时,及时更新相关记录。
|
- **主动更新**:每次完成一个任务或项目有任何变化时,及时更新相关记录。
|
||||||
- **保持可读**:老板也会参与 @aimemo/ 的查阅、编写、更新,请确保内容清晰、信息同步。
|
- **保持可读**:老板也会参与 @aimemo/ 的查阅、编写、更新,请确保内容清晰、信息同步。
|
||||||
|
- **日期作为文件名前缀**:额外添加的文件,以创建日期 yyyymmdd\_ 作为文件名前缀,例如 20260901_newFeature.md,方便和基础框架文件区分开来。
|
||||||
|
|
||||||
## 工作规范
|
## 工作规范
|
||||||
|
|
||||||
每次修改并验证成功之后,自动做一次 git commit。但不要自动推送,直到老板明确要求推送才推送。
|
每次完成了一个完整的特性、修复了一个bug、做了一定量修改并验证成功之后,更新 worklog 并做一次 git commit,注意worklog的更新应当与其记录的其他更新一起提交。但如果是很微小的改动,特别是界面的改动,那就不必commit,先让老板验收。不要推送,除非老板明确要求。
|
||||||
|
|
||||||
|
你在一个协同工作的环境中。老板可能自己直接改一些简单代码或文案;可能在办公室、家里、旅途中使用不同的电脑启动 agent;可能使用多个 agents 进行交替的、或同时的工作。你要注意,记录及时、准确的文档,有利于互相协作;不要互相干扰,不要提交别人的工作,除非老板明确要求。
|
||||||
|
|
||||||
|
有需要机密信息(例如 apikey, password 等)时,你可向老板提出需求,老板通常会放在 @\_simsim.gitomit.sfomit/ 目录下供你使用。
|
||||||
|
|
||||||
## 命名规范
|
## 命名规范
|
||||||
|
|
||||||
|
|||||||
@@ -1,13 +1,18 @@
|
|||||||
// 功能:从 https://git.tic.cc/opx/sysconfig/ 同步 agents/AGENTS.md 和 agents/CLAUDE.md
|
// 功能:从 https://git.tic.cc/opx/sysconfig/ 同步 agents/AGENTS.md 和 agents/CLAUDE.md
|
||||||
// 到本项目根目录(覆盖现有文件)。
|
// 到本项目根目录(覆盖现有文件);并在当前目录初始化 aimemo/ 工作记忆目录,
|
||||||
|
// 按 AGENTS.md 约定创建缺失的 productLens.md / projectPlan.md / worklog.md / userProfile.md(空白文件),已存在的保持不动。
|
||||||
// 使用 Node 内置 fetch,跨 Windows / Linux / macOS 平台,无需额外工具。
|
// 使用 Node 内置 fetch,跨 Windows / Linux / macOS 平台,无需额外工具。
|
||||||
|
|
||||||
// 调用:在 package.json 的 scripts 里添加 "syncAgentRule": "node -e \"fetch('https://git.tic.cc/opx/sysconfig/raw/branch/main/agents/syncAgentRule.mjs').then(r=>{if(!r.ok)throw new Error('HTTP '+r.status);return r.text()}).then(async t=>{await import('data:text/javascript;base64,'+Buffer.from(t).toString('base64'))})\""
|
// 调用:在 package.json 的 scripts 里添加 "syncAgentRule": "node -e \"fetch('https://git.tic.cc/opx/sysconfig/raw/branch/main/agents/syncAgentRule.mjs').then(r=>{if(!r.ok)throw new Error('HTTP '+r.status);return r.text()}).then(async t=>{await import('data:text/javascript;base64,'+Buffer.from(t).toString('base64'))})\""
|
||||||
import { writeFile } from 'node:fs/promises';
|
import { writeFile, mkdir, access } from 'node:fs/promises';
|
||||||
|
|
||||||
const BASE_URL = 'https://git.tic.cc/opx/sysconfig/raw/branch/main/agents/';
|
const BASE_URL = 'https://git.tic.cc/opx/sysconfig/raw/branch/main/agents/';
|
||||||
const FILES = ['AGENTS.md', 'CLAUDE.md'];
|
const FILES = ['AGENTS.md', 'CLAUDE.md'];
|
||||||
|
|
||||||
|
// aimemo 工作记忆目录:按 AGENTS.md 约定初始化,仅在调用时的工作目录(process.cwd())下创建缺失文件,已存在的一律沿用
|
||||||
|
const AIMEMO_DIR = 'aimemo';
|
||||||
|
const AIMEMO_FILES = ['productLens.md', 'projectPlan.md', 'worklog.md', 'userProfile.md'];
|
||||||
|
|
||||||
for (const name of FILES) {
|
for (const name of FILES) {
|
||||||
const response = await fetch(BASE_URL + name);
|
const response = await fetch(BASE_URL + name);
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
@@ -18,3 +23,17 @@ for (const name of FILES) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
console.log('agents 文件同步完成');
|
console.log('agents 文件同步完成');
|
||||||
|
|
||||||
|
// 初始化 aimemo/(相对路径解析到调用时的工作目录,即 process.cwd())
|
||||||
|
await mkdir(AIMEMO_DIR, { recursive: true });
|
||||||
|
for (const name of AIMEMO_FILES) {
|
||||||
|
const filePath = `${AIMEMO_DIR}/${name}`;
|
||||||
|
try {
|
||||||
|
await access(filePath);
|
||||||
|
console.log(`已存在,沿用 ${filePath}`);
|
||||||
|
} catch {
|
||||||
|
await writeFile(filePath, '');
|
||||||
|
console.log(`已创建 ${filePath}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
console.log('aimemo 目录初始化完成');
|
||||||
|
|||||||
@@ -0,0 +1,43 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||||
|
<plist version="1.0">
|
||||||
|
<dict>
|
||||||
|
<key>PayloadContent</key>
|
||||||
|
<array>
|
||||||
|
<dict>
|
||||||
|
<key>DNSSettings</key>
|
||||||
|
<dict>
|
||||||
|
<key>DNSProtocol</key>
|
||||||
|
<string>HTTPS</string>
|
||||||
|
<key>ServerURL</key>
|
||||||
|
<string>https://dns.alidns.com/dns-query</string>
|
||||||
|
</dict>
|
||||||
|
<key>PayloadDisplayName</key>
|
||||||
|
<string>DoH AliDNS</string>
|
||||||
|
<key>PayloadIdentifier</key>
|
||||||
|
<string>cc.tic.einz.doh.alidns</string>
|
||||||
|
<key>PayloadType</key>
|
||||||
|
<string>com.apple.dnsSettings.managed</string>
|
||||||
|
<key>PayloadUUID</key>
|
||||||
|
<string>A1B2C3D4-E5F6-4A7B-8C9D-0E1F2A3B4C5D</string>
|
||||||
|
<key>PayloadVersion</key>
|
||||||
|
<integer>1</integer>
|
||||||
|
</dict>
|
||||||
|
</array>
|
||||||
|
<key>PayloadDisplayName</key>
|
||||||
|
<string>DoH AliDNS</string>
|
||||||
|
<key>PayloadIdentifier</key>
|
||||||
|
<string>cc.tic.einz.doh</string>
|
||||||
|
<key>PayloadRemovalDisallowed</key>
|
||||||
|
<false/>
|
||||||
|
<key>PayloadType</key>
|
||||||
|
<string>Configuration</string>
|
||||||
|
<key>PayloadUUID</key>
|
||||||
|
<string>B2C3D4E5-F6A7-4B8C-9D0E-1F2A3B4C5D6E</string>
|
||||||
|
<!-- macOS 26.1+ 必需:声明系统级范围,否则报"无法安装VPN服务负载"(系统bug) -->
|
||||||
|
<key>PayloadScope</key>
|
||||||
|
<string>System</string>
|
||||||
|
<key>PayloadVersion</key>
|
||||||
|
<integer>1</integer>
|
||||||
|
</dict>
|
||||||
|
</plist>
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||||
|
<plist version="1.0">
|
||||||
|
<dict>
|
||||||
|
<key>PayloadContent</key>
|
||||||
|
<array>
|
||||||
|
<dict>
|
||||||
|
<key>DNSSettings</key>
|
||||||
|
<dict>
|
||||||
|
<key>DNSProtocol</key>
|
||||||
|
<string>HTTPS</string>
|
||||||
|
<key>ServerURL</key>
|
||||||
|
<string>https://cloudflare-dns.com/dns-query</string>
|
||||||
|
</dict>
|
||||||
|
<key>PayloadDisplayName</key>
|
||||||
|
<string>DoH Cloudflare (1.1.1.1)</string>
|
||||||
|
<key>PayloadIdentifier</key>
|
||||||
|
<string>cc.tic.einz.doh.cloudflare</string>
|
||||||
|
<key>PayloadType</key>
|
||||||
|
<string>com.apple.dnsSettings.managed</string>
|
||||||
|
<key>PayloadUUID</key>
|
||||||
|
<string>E5F6A7B8-C9D0-4E1F-2A3B-4C5D6E7F8091</string>
|
||||||
|
<key>PayloadVersion</key>
|
||||||
|
<integer>1</integer>
|
||||||
|
</dict>
|
||||||
|
</array>
|
||||||
|
<key>PayloadDisplayName</key>
|
||||||
|
<string>DoH Cloudflare (1.1.1.1)</string>
|
||||||
|
<key>PayloadIdentifier</key>
|
||||||
|
<string>cc.tic.einz.doh.cloudflare</string>
|
||||||
|
<key>PayloadRemovalDisallowed</key>
|
||||||
|
<false/>
|
||||||
|
<key>PayloadType</key>
|
||||||
|
<string>Configuration</string>
|
||||||
|
<key>PayloadUUID</key>
|
||||||
|
<string>F6A7B8C9-D0E1-4F2A-3B4C-5D6E7F8091A2</string>
|
||||||
|
<!-- macOS 26.1+ 必需:声明系统级范围,否则报"无法安装VPN服务负载"(系统bug) -->
|
||||||
|
<key>PayloadScope</key>
|
||||||
|
<string>System</string>
|
||||||
|
<key>PayloadVersion</key>
|
||||||
|
<integer>1</integer>
|
||||||
|
</dict>
|
||||||
|
</plist>
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||||
|
<plist version="1.0">
|
||||||
|
<dict>
|
||||||
|
<key>PayloadContent</key>
|
||||||
|
<array>
|
||||||
|
<dict>
|
||||||
|
<key>DNSSettings</key>
|
||||||
|
<dict>
|
||||||
|
<key>DNSProtocol</key>
|
||||||
|
<string>HTTPS</string>
|
||||||
|
<key>ServerURL</key>
|
||||||
|
<string>https://doh.pub/dns-query</string>
|
||||||
|
</dict>
|
||||||
|
<key>PayloadDisplayName</key>
|
||||||
|
<string>DoH DNSPod (doh.pub)</string>
|
||||||
|
<key>PayloadIdentifier</key>
|
||||||
|
<string>cc.tic.einz.doh.dnspod</string>
|
||||||
|
<key>PayloadType</key>
|
||||||
|
<string>com.apple.dnsSettings.managed</string>
|
||||||
|
<key>PayloadUUID</key>
|
||||||
|
<string>C3D4E5F6-A7B8-4C9D-0E1F-2A3B4C5D6E7F</string>
|
||||||
|
<key>PayloadVersion</key>
|
||||||
|
<integer>1</integer>
|
||||||
|
</dict>
|
||||||
|
</array>
|
||||||
|
<key>PayloadDisplayName</key>
|
||||||
|
<string>DoH DNSPod (doh.pub)</string>
|
||||||
|
<key>PayloadIdentifier</key>
|
||||||
|
<string>cc.tic.einz.doh.dnspod</string>
|
||||||
|
<key>PayloadRemovalDisallowed</key>
|
||||||
|
<false/>
|
||||||
|
<key>PayloadType</key>
|
||||||
|
<string>Configuration</string>
|
||||||
|
<key>PayloadUUID</key>
|
||||||
|
<string>D4E5F6A7-B8C9-4D0E-1F2A-3B4C5D6E7F80</string>
|
||||||
|
<!-- macOS 26.1+ 必需:声明系统级范围,否则报"无法安装VPN服务负载"(系统bug) -->
|
||||||
|
<key>PayloadScope</key>
|
||||||
|
<string>System</string>
|
||||||
|
<key>PayloadVersion</key>
|
||||||
|
<integer>1</integer>
|
||||||
|
</dict>
|
||||||
|
</plist>
|
||||||
@@ -0,0 +1,190 @@
|
|||||||
|
#!/usr/bin/env node
|
||||||
|
// Cross-platform (node) port of git_ignore_find2merge.sh / .bat
|
||||||
|
//
|
||||||
|
// Search [ROOTPATH], merge [GLOBALPATH/.gitignore_global] and [ROOTPATH/<dir>/.gitignore.local.txt]
|
||||||
|
// into [ROOTPATH/<dir>/.gitignore], for every <dir> that has a .gitignore.
|
||||||
|
// (Judged from .gitignore, not from .git — some git repos keep privacy and hide .git.)
|
||||||
|
//
|
||||||
|
// usage:
|
||||||
|
// node git_ignore_find2merge.js [rootPath] [globalPath] [y|other]
|
||||||
|
// rootPath start of tree search, must be a directory (default: prompt, blank = current dir)
|
||||||
|
// globalPath path to .gitignore_global, or its parent dir, or an http(s) URL
|
||||||
|
// (default: https://git.tic.cc/opx/sysconfig/raw/branch/main/nixhome/.gitignore_global)
|
||||||
|
// yesNo 'y' to write, anything else = dry-run (list candidates without writing)
|
||||||
|
//
|
||||||
|
// node git_ignore_find2merge.js # interactive, dry-run
|
||||||
|
// node git_ignore_find2merge.js ~/Shared y
|
||||||
|
// node git_ignore_find2merge.js /srv/projects /srv/proj/nixhome y
|
||||||
|
//
|
||||||
|
// Same fixes as seafile_ignore_find2merge.js:
|
||||||
|
// - blank globalPath falls back to the default URL, as in the .sh
|
||||||
|
// - ask() treats stdin EOF as blank, so non-interactive pipes don't hang
|
||||||
|
// - the default global is a URL: fetched with fetch() at 30s timeout + one retry
|
||||||
|
// (the .sh's `cat $GLOBALPATH` branch never ran for URLs — it went to the curl branch)
|
||||||
|
// - the root dir itself is processed too (the .sh's find -name '[^.]*' never matched the root)
|
||||||
|
// - non-'y' is a dry-run that still lists the candidate dirs (the .sh just exited silently)
|
||||||
|
|
||||||
|
const fs = require('fs')
|
||||||
|
const os = require('os')
|
||||||
|
const path = require('path')
|
||||||
|
const readline = require('readline')
|
||||||
|
|
||||||
|
const DEFAULT_GLOBAL_URL = 'https://git.tic.cc/opx/sysconfig/raw/branch/main/nixhome/.gitignore_global'
|
||||||
|
const GLOBAL_FILE_NAME = '.gitignore_global'
|
||||||
|
// same skip list as: grep -E -v 'node_modules|uni_modules|\.deploy_git|\.git|.svn|\.vscode|\.wrangler|unpackage|_webroot|_logstore|_datasotre|_archive|_filestore|_ssl'
|
||||||
|
const SKIP_DIR_PATTERN = /node_modules|uni_modules|\.deploy_git|\.git|\.svn|\.vscode|\.wrangler|unpackage|_webroot|_logstore|_datasotre|_archive|_filestore|_ssl/
|
||||||
|
const MAX_DEPTH = 3 // relative to ROOTPATH, matching find -mindepth 0 -maxdepth 3
|
||||||
|
|
||||||
|
function resolveHome (p) {
|
||||||
|
if (p === '~') return os.homedir()
|
||||||
|
if (p.startsWith('~/')) return path.join(os.homedir(), p.slice(2))
|
||||||
|
return p
|
||||||
|
}
|
||||||
|
|
||||||
|
async function ask (question) {
|
||||||
|
const rl = readline.createInterface({ input: process.stdin, output: process.stdout })
|
||||||
|
const answer = await new Promise(resolve => {
|
||||||
|
let settled = false
|
||||||
|
const finish = value => { if (!settled) { settled = true; resolve(value) } }
|
||||||
|
rl.question(question, a => finish(a))
|
||||||
|
rl.on('close', () => finish('')) // stdin ended without input (EOF): treat as blank, like the .sh's `read`
|
||||||
|
})
|
||||||
|
rl.close()
|
||||||
|
return answer.trim()
|
||||||
|
}
|
||||||
|
|
||||||
|
// find . -mindepth 0 -maxdepth 3 -type d -name '[^.]*' (relative paths, root itself excluded)
|
||||||
|
function collectCandidateDirs (rootPath) {
|
||||||
|
const candidates = []
|
||||||
|
function walk (dir, depth) {
|
||||||
|
if (depth > MAX_DEPTH) return
|
||||||
|
let entries
|
||||||
|
try {
|
||||||
|
entries = fs.readdirSync(dir, { withFileTypes: true })
|
||||||
|
} catch {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
for (const entry of entries) {
|
||||||
|
if (!entry.isDirectory()) continue
|
||||||
|
if (entry.name.startsWith('.')) continue // -name '[^.]*' excludes hidden dirs
|
||||||
|
const absDir = path.join(dir, entry.name)
|
||||||
|
const relDir = path.relative(rootPath, absDir)
|
||||||
|
if (relDir.split(path.sep).length > MAX_DEPTH) continue
|
||||||
|
candidates.push(relDir)
|
||||||
|
walk(absDir, depth + 1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
walk(rootPath, 1)
|
||||||
|
return candidates
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadGlobalContent (globalPathInput) {
|
||||||
|
const globalPath = resolveHome(globalPathInput)
|
||||||
|
if (/^https?:\/\//i.test(globalPath)) {
|
||||||
|
console.log('√√√ GLOBALPATH (url) = [[' + globalPath + ']]')
|
||||||
|
// git.tic.cc's TCP connect can exceed node's default 10s connect timeout; allow 30s and retry once.
|
||||||
|
let lastError
|
||||||
|
for (let attempt = 1; attempt <= 2; attempt++) {
|
||||||
|
try {
|
||||||
|
const response = await fetch(globalPath, { signal: AbortSignal.timeout(30000) })
|
||||||
|
if (!response.ok) throw new Error('HTTP ' + response.status)
|
||||||
|
return await response.text()
|
||||||
|
} catch (error) {
|
||||||
|
lastError = error
|
||||||
|
console.log('... fetch attempt ' + attempt + ' failed: ' + (error.cause?.code || error.message))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
console.log('××× Fetch failed: ' + (lastError.cause?.code || lastError.message) + ' for [[' + globalPath + ']]. Exit now...')
|
||||||
|
process.exit(1)
|
||||||
|
}
|
||||||
|
let globalFile = globalPath
|
||||||
|
if (fs.existsSync(globalPath) && fs.statSync(globalPath).isDirectory()) {
|
||||||
|
globalFile = path.join(globalPath, GLOBAL_FILE_NAME)
|
||||||
|
}
|
||||||
|
if (!fs.existsSync(globalFile)) {
|
||||||
|
console.log('××× Not found [[' + globalFile + ']]. Exit now...')
|
||||||
|
process.exit(1)
|
||||||
|
}
|
||||||
|
console.log('√√√ GLOBALPATH = [[' + globalFile + ']]')
|
||||||
|
return fs.readFileSync(globalFile, 'utf8')
|
||||||
|
}
|
||||||
|
|
||||||
|
async function main () {
|
||||||
|
console.log('')
|
||||||
|
console.log('Search in [ROOTPATH], merge [GLOBALPATH/.gitignore_global] and [ROOTPATH/*/.gitignore.local.txt] files to .gitignore')
|
||||||
|
console.log('')
|
||||||
|
|
||||||
|
// --- ROOTPATH ---
|
||||||
|
let rootPath = process.argv[2]
|
||||||
|
const rootIsDir = p => {
|
||||||
|
try { return fs.statSync(p).isDirectory() } catch { return false }
|
||||||
|
}
|
||||||
|
if (!(rootPath && rootIsDir(resolveHome(rootPath)))) {
|
||||||
|
console.log('::*** Enter [root path] or [leave blank] for default [[' + process.cwd() + ']] to start tree search for git repositories')
|
||||||
|
rootPath = await ask('***:: ')
|
||||||
|
if (!rootPath) rootPath = process.cwd()
|
||||||
|
rootPath = path.resolve(resolveHome(rootPath))
|
||||||
|
} else {
|
||||||
|
rootPath = path.resolve(resolveHome(rootPath))
|
||||||
|
}
|
||||||
|
if (!rootIsDir(rootPath)) {
|
||||||
|
console.log('××× [[' + rootPath + ']] not exist! Exit now. ***')
|
||||||
|
process.exit(1)
|
||||||
|
}
|
||||||
|
console.log('√√√ ROOTPATH = [[' + rootPath + ']]')
|
||||||
|
console.log('')
|
||||||
|
|
||||||
|
// --- GLOBALPATH ---
|
||||||
|
let globalPathInput = process.argv[3]
|
||||||
|
if (!globalPathInput) {
|
||||||
|
console.log('::*** Enter [path to .gitignore_global] or [leave blank] for default [[' + DEFAULT_GLOBAL_URL + ']]')
|
||||||
|
globalPathInput = await ask('***:: ')
|
||||||
|
if (!globalPathInput) globalPathInput = DEFAULT_GLOBAL_URL // blank → default URL, as in the .sh
|
||||||
|
}
|
||||||
|
const globalContent = await loadGlobalContent(globalPathInput)
|
||||||
|
console.log('')
|
||||||
|
|
||||||
|
// --- y/N ---
|
||||||
|
let yesNo = process.argv[4]
|
||||||
|
if (!yesNo) {
|
||||||
|
console.log('::*** Enter [y] to start updating, or [anything else] for dry-run')
|
||||||
|
yesNo = await ask('***:: ')
|
||||||
|
}
|
||||||
|
const willWrite = yesNo === 'y'
|
||||||
|
|
||||||
|
console.log('*** Starting from [[' + rootPath + ']] ***')
|
||||||
|
console.log('')
|
||||||
|
|
||||||
|
// --- search + merge ---
|
||||||
|
// A dir is a candidate iff it has a .gitignore (judged from .gitignore, not from .git).
|
||||||
|
const candidateDirs = collectCandidateDirs(rootPath).sort()
|
||||||
|
// The root dir itself is also a candidate: the old .sh's find -name '[^.]*' never matched the root.
|
||||||
|
if (fs.existsSync(path.join(rootPath, '.gitignore'))) candidateDirs.unshift('.')
|
||||||
|
|
||||||
|
let updatedCount = 0
|
||||||
|
for (const relDir of candidateDirs) {
|
||||||
|
const absDir = relDir === '.' ? rootPath : path.join(rootPath, relDir)
|
||||||
|
if (relDir !== '.' && SKIP_DIR_PATTERN.test(relDir)) continue
|
||||||
|
const targetFile = path.join(absDir, '.gitignore')
|
||||||
|
const localFile = path.join(absDir, '.gitignore.local.txt')
|
||||||
|
if (!fs.existsSync(targetFile)) continue
|
||||||
|
console.log('---- updating [[' + relDir + '/.gitignore]] ----')
|
||||||
|
if (willWrite) {
|
||||||
|
let merged = globalContent
|
||||||
|
if (fs.existsSync(localFile)) merged += fs.readFileSync(localFile, 'utf8')
|
||||||
|
fs.writeFileSync(targetFile, merged)
|
||||||
|
updatedCount++
|
||||||
|
}
|
||||||
|
console.log('')
|
||||||
|
}
|
||||||
|
if (willWrite) {
|
||||||
|
console.log('*** done: ' + updatedCount + ' file(s) updated ***')
|
||||||
|
} else {
|
||||||
|
console.log('*** dry-run: no file written (enter y to update) ***')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
main().catch(err => {
|
||||||
|
console.error(err)
|
||||||
|
process.exit(1)
|
||||||
|
})
|
||||||
@@ -9,4 +9,4 @@ fi
|
|||||||
echo Public IP: `curl -s ifconfig.me`
|
echo Public IP: `curl -s ifconfig.me`
|
||||||
echo
|
echo
|
||||||
|
|
||||||
curl ipinfo.io
|
net.sh
|
||||||
|
|||||||
@@ -0,0 +1,7 @@
|
|||||||
|
echo https://ipinfo.io/country == $(curl -s https://ipinfo.io/country) # 返回国家代码,例如 CN, DE, US
|
||||||
|
echo https://myip.ipip.net == $(curl -s https://myip.ipip.net) # 国内站应显示无锡/电信(直连)
|
||||||
|
echo https://ipinfo.io/json ==
|
||||||
|
curl -s https://ipinfo.io/json # 详情:IP、城市、组织
|
||||||
|
# curl ipinfo.io
|
||||||
|
|
||||||
|
echo
|
||||||
+2
-2
@@ -146,9 +146,9 @@ elif [ -f /etc/ubuntu_version ]; then
|
|||||||
MYOSVERSION=Ubt`cat /etc/ubuntu_version 2>/dev/null`
|
MYOSVERSION=Ubt`cat /etc/ubuntu_version 2>/dev/null`
|
||||||
fi
|
fi
|
||||||
if [ "$color_prompt" = yes ]; then
|
if [ "$color_prompt" = yes ]; then
|
||||||
PS1='\n¶ \[\033[$PSTYLE;${PTYPE}2m\]\t\[\033[00m\] \[\033[$PSTYLE;${PTYPE}5m\]\u\[\033[00m\] @\[\033[$PSTYLE;${PTYPE}2m\]\h\[\033[00m\] =\[\033[$PSTYLE;${PTYPE}5m\]$MYIPPUB\[\033[00m\] #\[\033[$PSTYLE;${PTYPE}2m\]$(uname -m),$(uname),$MYOSVERSION\[\033[00m\] \[\033[$PSTYLE;${PTYPE}5m\]$PWD/\[\033[00m\] \n[ \[\033[05;31m\]\W/\[\033[00m\] ]§ '
|
PS1='\n#[ \[\033[$PSTYLE;${PTYPE}2m\]\t\[\033[00m\] \[\033[$PSTYLE;${PTYPE}5m\]\u\[\033[00m\] @\[\033[$PSTYLE;${PTYPE}2m\]\h\[\033[00m\] =\[\033[$PSTYLE;${PTYPE}5m\]$MYIPPUB\[\033[00m\] #\[\033[$PSTYLE;${PTYPE}2m\]$(uname -m),$(uname),$MYOSVERSION\[\033[00m\] \[\033[$PSTYLE;${PTYPE}5m\]$PWD/\[\033[00m\] ]#\n#[ \[\033[05;31m\]\W/\[\033[00m\] ]# '
|
||||||
else
|
else
|
||||||
PS1='\n¶ \t \u @\h =$MYIPPUB #$(uname -m),$(uname),$MYOSVERSION $PWD/ \n[ \W/ ]§ '
|
PS1='\n#[ \t \u @\h =$MYIPPUB #$(uname -m),$(uname),$MYOSVERSION $PWD/ ]#\n#[ \W/ ]# '
|
||||||
fi
|
fi
|
||||||
unset color_prompt force_color_prompt
|
unset color_prompt force_color_prompt
|
||||||
|
|
||||||
|
|||||||
@@ -94,6 +94,8 @@ OneDrive/Icon
|
|||||||
*/.dart_tool/
|
*/.dart_tool/
|
||||||
.flutter-plugins-dependencies
|
.flutter-plugins-dependencies
|
||||||
*/.flutter-plugins-dependencies
|
*/.flutter-plugins-dependencies
|
||||||
|
build/
|
||||||
|
*/build/
|
||||||
|
|
||||||
# ---- iOS / macOS:CocoaPods 检出目录(Podfile.lock 仍提交共享,只忽略 Pods/ 实体)----
|
# ---- iOS / macOS:CocoaPods 检出目录(Podfile.lock 仍提交共享,只忽略 Pods/ 实体)----
|
||||||
Pods/
|
Pods/
|
||||||
|
|||||||
@@ -41,6 +41,4 @@ echo all_proxy=$all_proxy
|
|||||||
echo http_proxy=$http_proxy
|
echo http_proxy=$http_proxy
|
||||||
echo https_proxy=$https_proxy
|
echo https_proxy=$https_proxy
|
||||||
|
|
||||||
# ip.gs, ip.sb, ipinfo.io
|
net.sh
|
||||||
curl ipinfo.io
|
|
||||||
echo " "
|
|
||||||
|
|||||||
@@ -0,0 +1,59 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# route.sh 域名 —— 输出该域名在 mihomo 的分流结果,如: DIRECT / PROXY[de1-vless] / REJECT
|
||||||
|
# 用法: route.sh www.xiaohongshu.com [更多域名...]
|
||||||
|
# 自适应两套环境(同一份脚本,sysconfig 同步到所有电脑):
|
||||||
|
# 1) Mac + Clash Verge service:unix socket + 7897(无 TCP API、无需 secret)
|
||||||
|
# 2) Linux 服务器(如 cn1):TCP API + secret + 7890
|
||||||
|
# 可靠性设计:每域名独立一条 8s 日志流(长流/连续流会被内核丢弃),流内 grep 纯域名
|
||||||
|
#(JSON 里 "-->" 被转义成 \u003e,不能按 "--> " 前缀匹配)
|
||||||
|
|
||||||
|
# ==== ENV(按需修改)====
|
||||||
|
MAC_SOCK="/var/run/clash-verge-service/users/501/verge-mihomo.sock" # Verge service unix socket
|
||||||
|
CN1_API="http://100.100.86.1:9090" # cn1 mihomo REST API
|
||||||
|
CN1_SECRET="pl1pool-2026" # cn1 API secret
|
||||||
|
PROXY_MAC="http://127.0.0.1:7897" # Mac mixed 端口
|
||||||
|
PROXY_CN1="http://127.0.0.1:7890" # cn1 mixed 端口
|
||||||
|
# ========================
|
||||||
|
|
||||||
|
command -v curl >/dev/null || { echo "curl is required"; exit 1; }
|
||||||
|
|
||||||
|
# ---- 探测环境:优先 unix socket(Mac),否则 TCP+secret(cn1/其他)----
|
||||||
|
if [ -S "$MAC_SOCK" ]; then
|
||||||
|
MODE="mac"
|
||||||
|
SOCK_ARGS=(--unix-socket "$MAC_SOCK")
|
||||||
|
API_URL="http://localhost"
|
||||||
|
AUTH=()
|
||||||
|
PROXY="$PROXY_MAC"
|
||||||
|
else
|
||||||
|
MODE="tcp"
|
||||||
|
SOCK_ARGS=()
|
||||||
|
API_URL="$CN1_API"
|
||||||
|
AUTH=(-H "Authorization: Bearer $CN1_SECRET")
|
||||||
|
PROXY="$PROXY_CN1"
|
||||||
|
PING=$(curl -s -m 5 "${AUTH[@]}" -o /dev/null -w '%{http_code}' "$API_URL/version" 2>/dev/null)
|
||||||
|
[ "$PING" = "200" ] || { echo "API unreachable: $API_URL (code=$PING) — 检查网络/secret"; exit 1; }
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ $# -eq 0 ]; then
|
||||||
|
echo "Usage: $(basename $0) domain [domain2 ...] (mode: $MODE)"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# 每域名独立短会话:8s 日志流 + 1s 后请求 + 7s 收尾(此形态实测最稳)
|
||||||
|
for domain in "$@"; do
|
||||||
|
LOG=$(mktemp)
|
||||||
|
curl -s --max-time 8 "${SOCK_ARGS[@]}" "${AUTH[@]}" "$API_URL/logs?level=info" > "$LOG" 2>/dev/null &
|
||||||
|
LPID=$!
|
||||||
|
sleep 1
|
||||||
|
curl -s -m 5 -x "$PROXY" -o /dev/null "https://$domain" 2>/dev/null
|
||||||
|
sleep 7
|
||||||
|
LINE=$(grep -m1 "$domain" "$LOG" 2>/dev/null | head -1)
|
||||||
|
if [ -z "$LINE" ]; then
|
||||||
|
printf "%-40s ?? (no match line captured)\n" "$domain"
|
||||||
|
else
|
||||||
|
RESULT=$(echo "$LINE" | sed -n 's/.* match .* using //p' | sed 's/"}$//' | sed 's/"$//')
|
||||||
|
RULE=$(echo "$LINE" | sed -n 's/.* match \(.*\) using .*/\1/p')
|
||||||
|
printf "%-40s %-24s (rule: %s)\n" "$domain" "${RESULT:-??}" "${RULE:-?}"
|
||||||
|
fi
|
||||||
|
kill $LPID 2>/dev/null; wait $LPID 2>/dev/null; rm -f "$LOG"
|
||||||
|
done
|
||||||
+3
-1
@@ -87,13 +87,15 @@ OneDrive/Icon
|
|||||||
# Flutter/Dart 机器本地产物(各端由 `flutter pub get` / `pod install` / 构建在本机重生成,不跨机同步)。
|
# Flutter/Dart 机器本地产物(各端由 `flutter pub get` / `pod install` / 构建在本机重生成,不跨机同步)。
|
||||||
# 双机开发时 .dart_tool/package_config.json 内是绝对路径(file:///Users/<用户>/.pub-cache/...),同步过去会让另一台机器编译报 No such file or directory。
|
# 双机开发时 .dart_tool/package_config.json 内是绝对路径(file:///Users/<用户>/.pub-cache/...),同步过去会让另一台机器编译报 No such file or directory。
|
||||||
# 注:pub 缓存在 ~/.pub-cache,位于资料库根之外,seafile 同步不到,无需在此列出。
|
# 注:pub 缓存在 ~/.pub-cache,位于资料库根之外,seafile 同步不到,无需在此列出。
|
||||||
# 注:build/ 未全局忽略——tic-www-vue / cox-user-vue / cox-team-vue 等把 build/ 纳入 git 跟踪,全局忽略会波及;按需放各项目 seafile-ignore.local.txt。
|
# 注:build/ 未全局忽略,以防波及用了该常见词的目录;按需放各项目 seafile-ignore.local.txt。
|
||||||
|
|
||||||
# ---- Dart / Flutter:包解析 + 构建产物(核心,换机必跑 `flutter pub get` 重生成)----
|
# ---- Dart / Flutter:包解析 + 构建产物(核心,换机必跑 `flutter pub get` 重生成)----
|
||||||
.dart_tool/
|
.dart_tool/
|
||||||
*/.dart_tool/
|
*/.dart_tool/
|
||||||
.flutter-plugins-dependencies
|
.flutter-plugins-dependencies
|
||||||
*/.flutter-plugins-dependencies
|
*/.flutter-plugins-dependencies
|
||||||
|
build/
|
||||||
|
*/build/
|
||||||
|
|
||||||
# ---- iOS / macOS:CocoaPods 检出目录(Podfile.lock 仍提交共享,只忽略 Pods/ 实体)----
|
# ---- iOS / macOS:CocoaPods 检出目录(Podfile.lock 仍提交共享,只忽略 Pods/ 实体)----
|
||||||
Pods/
|
Pods/
|
||||||
|
|||||||
@@ -20,6 +20,7 @@
|
|||||||
// - the root dir itself is processed too (the .sh's find -name '[^.]*' never matched the root dir)
|
// - the root dir itself is processed too (the .sh's find -name '[^.]*' never matched the root dir)
|
||||||
// - a dir is updated iff it has seafile-ignore.txt OR seafile-ignore.local.txt (the .sh's `.git` check was dropped)
|
// - a dir is updated iff it has seafile-ignore.txt OR seafile-ignore.local.txt (the .sh's `.git` check was dropped)
|
||||||
// - tree search is max depth 3, skipping hidden dirs and the known build/dep dirs
|
// - tree search is max depth 3, skipping hidden dirs and the known build/dep dirs
|
||||||
|
// - blank globalPath falls back to the default URL, as in the .sh (first draft missed this → "Not found [[]]")
|
||||||
// - the .sh does `cat $GLOBALPATH` which fails when the default is a URL; here http(s) is fetched with fetch(),
|
// - the .sh does `cat $GLOBALPATH` which fails when the default is a URL; here http(s) is fetched with fetch(),
|
||||||
// a local file / parent-dir keeps the original behavior.
|
// a local file / parent-dir keeps the original behavior.
|
||||||
|
|
||||||
@@ -41,7 +42,12 @@ function resolveHome (p) {
|
|||||||
|
|
||||||
async function ask (question) {
|
async function ask (question) {
|
||||||
const rl = readline.createInterface({ input: process.stdin, output: process.stdout })
|
const rl = readline.createInterface({ input: process.stdin, output: process.stdout })
|
||||||
const answer = await new Promise(resolve => rl.question(question, resolve))
|
const answer = await new Promise(resolve => {
|
||||||
|
let settled = false
|
||||||
|
const finish = value => { if (!settled) { settled = true; resolve(value) } }
|
||||||
|
rl.question(question, a => finish(a))
|
||||||
|
rl.on('close', () => finish('')) // stdin ended without input (EOF): treat as blank, like the .sh's `read`
|
||||||
|
})
|
||||||
rl.close()
|
rl.close()
|
||||||
return answer.trim()
|
return answer.trim()
|
||||||
}
|
}
|
||||||
@@ -75,12 +81,20 @@ async function loadGlobalContent (globalPathInput) {
|
|||||||
const globalPath = resolveHome(globalPathInput)
|
const globalPath = resolveHome(globalPathInput)
|
||||||
if (/^https?:\/\//i.test(globalPath)) {
|
if (/^https?:\/\//i.test(globalPath)) {
|
||||||
console.log('√√√ GLOBALPATH (url) = [[' + globalPath + ']]')
|
console.log('√√√ GLOBALPATH (url) = [[' + globalPath + ']]')
|
||||||
const response = await fetch(globalPath)
|
// git.tic.cc's TCP connect can exceed node's default 10s connect timeout; allow 30s and retry once.
|
||||||
if (!response.ok) {
|
let lastError
|
||||||
console.log('××× Fetch failed: HTTP ' + response.status + ' for [[' + globalPath + ']]. Exit now...')
|
for (let attempt = 1; attempt <= 2; attempt++) {
|
||||||
process.exit(1)
|
try {
|
||||||
|
const response = await fetch(globalPath, { signal: AbortSignal.timeout(30000) })
|
||||||
|
if (!response.ok) throw new Error('HTTP ' + response.status)
|
||||||
|
return await response.text()
|
||||||
|
} catch (error) {
|
||||||
|
lastError = error
|
||||||
|
console.log('... fetch attempt ' + attempt + ' failed: ' + (error.cause?.code || error.message))
|
||||||
}
|
}
|
||||||
return response.text()
|
}
|
||||||
|
console.log('××× Fetch failed: ' + (lastError.cause?.code || lastError.message) + ' for [[' + globalPath + ']]. Exit now...')
|
||||||
|
process.exit(1)
|
||||||
}
|
}
|
||||||
let globalFile = globalPath
|
let globalFile = globalPath
|
||||||
if (fs.existsSync(globalPath) && fs.statSync(globalPath).isDirectory()) {
|
if (fs.existsSync(globalPath) && fs.statSync(globalPath).isDirectory()) {
|
||||||
@@ -119,6 +133,7 @@ async function main () {
|
|||||||
if (!globalPathInput) {
|
if (!globalPathInput) {
|
||||||
console.log('::*** Enter [path to seafile-ignore.global.txt] or [leave blank] for default [[' + DEFAULT_GLOBAL_URL + ']]')
|
console.log('::*** Enter [path to seafile-ignore.global.txt] or [leave blank] for default [[' + DEFAULT_GLOBAL_URL + ']]')
|
||||||
globalPathInput = await ask('***:: ')
|
globalPathInput = await ask('***:: ')
|
||||||
|
if (!globalPathInput) globalPathInput = DEFAULT_GLOBAL_URL // blank → default URL, as in the .sh
|
||||||
}
|
}
|
||||||
const globalContent = await loadGlobalContent(globalPathInput)
|
const globalContent = await loadGlobalContent(globalPathInput)
|
||||||
console.log('')
|
console.log('')
|
||||||
|
|||||||
@@ -19,6 +19,10 @@ if [[ ! -f "$SETTINGS_FILE" ]]; then
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
# Parse the JSON to get labels and corresponding details
|
# Parse the JSON to get labels and corresponding details
|
||||||
|
# 临时把 IFS 设为换行,让 =($(...)) 只按行拆分、保留 label 内部空格,
|
||||||
|
# 避免默认 IFS(空格) 拆散含空格的 label,导致每台显示两行、labels/hosts 错位
|
||||||
|
oldIFS=$IFS
|
||||||
|
IFS=$'\n'
|
||||||
labels=($(jq -r '.["sshfs.configs"][] | .label' "$SETTINGS_FILE"))
|
labels=($(jq -r '.["sshfs.configs"][] | .label' "$SETTINGS_FILE"))
|
||||||
hosts=($(jq -r '.["sshfs.configs"][] | .host' "$SETTINGS_FILE"))
|
hosts=($(jq -r '.["sshfs.configs"][] | .host' "$SETTINGS_FILE"))
|
||||||
ports=($(jq -r '.["sshfs.configs"][] | .port' "$SETTINGS_FILE"))
|
ports=($(jq -r '.["sshfs.configs"][] | .port' "$SETTINGS_FILE"))
|
||||||
@@ -26,13 +30,12 @@ names=($(jq -r '.["sshfs.configs"][] | .name' "$SETTINGS_FILE"))
|
|||||||
usernames=($(jq -r '.["sshfs.configs"][] | .username' "$SETTINGS_FILE"))
|
usernames=($(jq -r '.["sshfs.configs"][] | .username' "$SETTINGS_FILE"))
|
||||||
passwords=($(jq -r '.["sshfs.configs"][] | .password' "$SETTINGS_FILE"))
|
passwords=($(jq -r '.["sshfs.configs"][] | .password' "$SETTINGS_FILE"))
|
||||||
privateKeyPaths=($(jq -r '.["sshfs.configs"][] | .privateKeyPath' "$SETTINGS_FILE"))
|
privateKeyPaths=($(jq -r '.["sshfs.configs"][] | .privateKeyPath' "$SETTINGS_FILE"))
|
||||||
|
IFS=$oldIFS
|
||||||
|
|
||||||
options=()
|
|
||||||
prefixes=()
|
prefixes=()
|
||||||
for i in "${!labels[@]}"; do
|
for i in "${!labels[@]}"; do
|
||||||
option=$(echo "${labels[$i]}" | sed 's/[^[:alnum:]].*//') # 提取前几个字母和数字
|
option=$(echo "${labels[$i]}" | sed 's/[^[:alnum:]].*//') # 提取前缀(前导字母数字)
|
||||||
options+=("$option: ${labels[$i]}") # 去掉序号,保留前缀和完整label
|
prefixes+=("$option")
|
||||||
prefixes+=("$option") # 保存前缀
|
|
||||||
done
|
done
|
||||||
|
|
||||||
# Check if a command-line argument is provided
|
# Check if a command-line argument is provided
|
||||||
@@ -40,16 +43,26 @@ if [ "$1" != "" ]; then
|
|||||||
# 如果提供了参数,则直接使用这个前缀
|
# 如果提供了参数,则直接使用这个前缀
|
||||||
host_to_ssh=$1
|
host_to_ssh=$1
|
||||||
else
|
else
|
||||||
echo "Select a target to connect via SSH, or press 0 to exit:"
|
echo "Select a target to connect via SSH (Enter to exit):"
|
||||||
echo
|
echo
|
||||||
|
|
||||||
# Display options without numbers
|
# 对齐前缀列:按最长前缀宽度填充
|
||||||
for opt in "${options[@]}"; do
|
maxlen=0
|
||||||
echo "$opt"
|
for p in "${prefixes[@]}"; do
|
||||||
|
[ ${#p} -gt $maxlen ] && maxlen=${#p}
|
||||||
|
done
|
||||||
|
|
||||||
|
for i in "${!labels[@]}"; do
|
||||||
|
printf "%-*s: %s\n" "$maxlen" "${prefixes[$i]}" "${labels[$i]}"
|
||||||
done
|
done
|
||||||
|
|
||||||
echo
|
echo
|
||||||
read -p "请输入要连接的主机前缀: " host_to_ssh
|
read -p "请输入要连接的主机前缀 (直接回车退出): " host_to_ssh
|
||||||
|
# 空输入直接退出(空前缀会误匹配到错乱的数组项,直接拦截)
|
||||||
|
if [ -z "$host_to_ssh" ]; then
|
||||||
|
echo "已退出。"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
fi
|
fi
|
||||||
|
|
||||||
echo
|
echo
|
||||||
|
|||||||
@@ -0,0 +1 @@
|
|||||||
|
fetch('https://git.tic.cc/opx/sysconfig/raw/branch/main/agents/syncAgentRule.mjs').then(r=>{if(!r.ok)throw new Error('HTTP '+r.status);return r.text()}).then(async t=>{await import('data:text/javascript;base64,'+Buffer.from(t).toString('base64'))})
|
||||||
Reference in New Issue
Block a user