From 5fa2782f028679d74013b2008de7fe1afa137d0f Mon Sep 17 00:00:00 2001 From: chendelian <116870791@qq.com> Date: Sun, 22 Mar 2026 15:48:19 +0800 Subject: [PATCH 01/27] first commit --- SKILL.md | 54 ++++++++++++++++++ accounts.json | 81 +++++++++++++++++++++++++++ scripts/account.py | 134 +++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 269 insertions(+) create mode 100644 SKILL.md create mode 100644 accounts.json create mode 100644 scripts/account.py diff --git a/SKILL.md b/SKILL.md new file mode 100644 index 0000000..dcf117d --- /dev/null +++ b/SKILL.md @@ -0,0 +1,54 @@ +--- +name: account-manager +description: 多平台多账号管理。管理各平台账号与Chrome Profile的对应关系,供publisher类Skill调用获取账号信息。 +version: 1.0.0 +author: 深圳匠厂科技有限公司 +metadata: + openclaw: + emoji: "👤" +allowed-tools: + - bash +--- + +# 账号管理器 + +## 使用时机 + +当用户发送以下内容时触发本Skill: +- 说"初始化账号登录"、"登录搜狐账号"、"帮我登录账号" +- 说"列出账号"、"查看所有账号"、"有哪些账号" +- 说"添加账号"、"删除账号"、"获取账号信息" + +## 执行步骤 + +### 列出某平台所有账号 +```bash +python3 {baseDir}/scripts/account.py list +``` + +### 获取某个账号详情 +```bash +python3 {baseDir}/scripts/account.py get +``` + +### 初始化账号登录(首次使用,打开浏览器手动登录) +```bash +python3 {baseDir}/scripts/account.py login +``` + +## 输出格式 + +- list:逐行返回账号ID和名称 +- get:返回账号完整信息 +- login:打开浏览器后提示用户手动完成登录 + +## 错误处理 + +- 账号不存在:提示"未找到该账号,请检查账号ID是否正确" +- 平台无账号:提示"该平台暂无账号,请先在accounts.json中添加" + +## 注意事项 + +- 账号档案存储在 `accounts.json`,新增账号需手动编辑该文件 +- Chrome Profile目录首次登录后自动创建,登录态永久保存 +- 每个账号只需初始化登录一次 \ No newline at end of file diff --git a/accounts.json b/accounts.json new file mode 100644 index 0000000..dde789e --- /dev/null +++ b/accounts.json @@ -0,0 +1,81 @@ +{ + "sohu": [ + { + "id": "sohu_account1", + "name": "搜狐账号1", + "phone": "13800000000", + "platform": "sohu", + "profile_dir": "D:/OpenClaw/chrome-profiles/sohu_account1" + }, + { + "id": "sohu_account2", + "name": "搜狐账号2", + "phone": "13900000000", + "platform": "sohu", + "profile_dir": "D:/OpenClaw/chrome-profiles/sohu_account2" + } + ], + "zhihu": [ + { + "id": "zhihu_account1", + "name": "知乎账号1", + "phone": "", + "platform": "zhihu", + "profile_dir": "D:/OpenClaw/chrome-profiles/zhihu_account1" + } + ], + "kimi": [ + { + "id": "kimi_account1", + "name": "Kimi账号1", + "phone": "13800000000", + "platform": "kimi", + "profile_dir": "D:/OpenClaw/chrome-profiles/kimi_account1" + } + ], + "deepseek": [ + { + "id": "deepseek_account1", + "name": "DeepSeek账号1", + "phone": "", + "platform": "deepseek", + "profile_dir": "D:/OpenClaw/chrome-profiles/deepseek_account1" + } + ], + "doubao": [ + { + "id": "doubao_account1", + "name": "豆包账号1", + "phone": "", + "platform": "doubao", + "profile_dir": "D:/OpenClaw/chrome-profiles/doubao_account1" + } + ], + "qianwen": [ + { + "id": "qianwen_account1", + "name": "千问账号1", + "phone": "", + "platform": "qianwen", + "profile_dir": "D:/OpenClaw/chrome-profiles/qianwen_account1" + } + ], + "yiyan": [ + { + "id": "yiyan_account1", + "name": "一言账号1", + "phone": "", + "platform": "yiyan", + "profile_dir": "D:/OpenClaw/chrome-profiles/yiyan_account1" + } + ], + "yuanbao": [ + { + "id": "yuanbao_account1", + "name": "元宝账号1", + "phone": "", + "platform": "yuanbao", + "profile_dir": "D:/OpenClaw/chrome-profiles/yuanbao_account1" + } + ] +} \ No newline at end of file diff --git a/scripts/account.py b/scripts/account.py new file mode 100644 index 0000000..85d6667 --- /dev/null +++ b/scripts/account.py @@ -0,0 +1,134 @@ +import sys +import json +import os +import subprocess + +# Windows GBK 编码兼容修复 +if sys.platform == "win32": + import io + sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8', errors='replace') + sys.stderr = io.TextIOWrapper(sys.stderr.buffer, encoding='utf-8', errors='replace') + +BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) +ACCOUNTS_FILE = os.path.join(BASE_DIR, "accounts.json") + +PLATFORM_URLS = { + # 自媒体/图文平台 + "sohu": "https://mp.sohu.com", + "zhihu": "https://www.zhihu.com", + "wechat": "https://mp.weixin.qq.com", + + # 大模型平台 + "kimi": "https://kimi.moonshot.cn", + "deepseek": "https://chat.deepseek.com", + "doubao": "https://www.doubao.com", + "qianwen": "https://tongyi.aliyun.com", + "yiyan": "https://yiyan.baidu.com", + "yuanbao": "https://yuanbao.tencent.com" +} + +def load_accounts(): + with open(ACCOUNTS_FILE, "r", encoding="utf-8") as f: + return json.load(f) + +def cmd_list(platform="all"): + """ + 修改后的 list 功能,能直观打出带着【手机号】的详细台账供AI查询匹配! + 支持查全部(all),或指定(sohu) + """ + accounts = load_accounts() + if platform == "all": + platforms_to_check = accounts.keys() + else: + platforms_to_check = [platform] + + found = False + for plat in platforms_to_check: + platform_accounts = accounts.get(plat, []) + for acc in platform_accounts: + phone = acc.get('phone', '未绑定手机') + print(f"账号ID:{acc['id']} | 名称:{acc['name']} | 手机号:{phone} | 平台:{acc['platform']}") + found = True + + if not found: + print(f"ERROR:NO_ACCOUNTS_FOUND") + + +def cmd_get(account_id): + accounts = load_accounts() + for platform_accounts in accounts.values(): + for acc in platform_accounts: + if acc["id"] == account_id: + print(json.dumps(acc, ensure_ascii=False)) + return + print("ERROR:ACCOUNT_NOT_FOUND") + +def cmd_login(account_id): + """首次登录:打开浏览器,用户手动登录,登录态自动保存到Profile目录""" + accounts = load_accounts() + target = None + for platform_accounts in accounts.values(): + for acc in platform_accounts: + if acc["id"] == account_id: + target = acc + break + + if not target: + print("ERROR:ACCOUNT_NOT_FOUND") + return + + profile_dir = target["profile_dir"] + os.makedirs(profile_dir, exist_ok=True) + url = PLATFORM_URLS.get(target["platform"], "https://www.google.com") + + print(f"正在为账号 [{target['name']}] 打开浏览器...") + print(f"请在浏览器中完成登录,登录后直接关闭浏览器窗口即可。") + print(f"登录态将自动保存到:{profile_dir}") + + # 用Playwright打开持久化浏览器,等待用户手动登录 + login_script = f""" +import asyncio +from playwright.async_api import async_playwright +async def main(): + async with async_playwright() as p: + browser = await p.chromium.launch_persistent_context( + user_data_dir=r"{profile_dir}", + headless=False, + channel="chrome", # 【修改处】也加上这行 + no_viewport=True, # 确保浏览器内容视口也最大化 + args=["--start-maximized"] + ) + page = browser.pages[0] if browser.pages else await browser.new_page() + await page.goto("{url}") + print("浏览器已打开,请完成登录后关闭窗口...") + # 移除自动超时,无限等待直到用户扫码完手动关闭 + await browser.wait_for_event("close", timeout=0) + print("登录态已保存!") +asyncio.run(main()) +""" + + import tempfile + with tempfile.NamedTemporaryFile(mode='w', suffix='.py', + delete=False, encoding='utf-8') as f: + f.write(login_script) + tmp_path = f.name + + os.system(f'python "{tmp_path}"') + os.unlink(tmp_path) + +if __name__ == "__main__": + if len(sys.argv) < 2: + print("用法:python account.py list [platform] | get | login ") + sys.exit(1) + + cmd = sys.argv[1] + if cmd == "list": + # 默认不传 platform 时打出 all + cmd_list(sys.argv[2] if len(sys.argv) >= 3 else "all") + elif cmd == "get" and len(sys.argv) >= 3: + cmd_get(sys.argv[2]) + elif cmd == "login" and len(sys.argv) >= 3: + cmd_login(sys.argv[2]) + else: + print("参数错误") + sys.exit(1) \ No newline at end of file From 1fcb676d353541d3a99b593b6a6f3a7c9270fb9a Mon Sep 17 00:00:00 2001 From: chendelian <116870791@qq.com> Date: Mon, 23 Mar 2026 18:30:29 +0800 Subject: [PATCH 02/27] Fix account-manager merge conflicts Made-with: Cursor --- scripts/account.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/scripts/account.py b/scripts/account.py index 85d6667..17a79de 100644 --- a/scripts/account.py +++ b/scripts/account.py @@ -8,7 +8,6 @@ if sys.platform == "win32": import io sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8', errors='replace') sys.stderr = io.TextIOWrapper(sys.stderr.buffer, encoding='utf-8', errors='replace') - BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) ACCOUNTS_FILE = os.path.join(BASE_DIR, "accounts.json") @@ -53,7 +52,6 @@ def cmd_list(platform="all"): if not found: print(f"ERROR:NO_ACCOUNTS_FOUND") - def cmd_get(account_id): accounts = load_accounts() for platform_accounts in accounts.values(): @@ -106,7 +104,6 @@ async def main(): print("登录态已保存!") asyncio.run(main()) """ - import tempfile with tempfile.NamedTemporaryFile(mode='w', suffix='.py', delete=False, encoding='utf-8') as f: From 6f44c001b1fdd42e48d4519ce492b3cad48aba12 Mon Sep 17 00:00:00 2001 From: chendelian <116870791@qq.com> Date: Mon, 23 Mar 2026 18:34:05 +0800 Subject: [PATCH 03/27] Fix account-manager merge conflicts --- scripts/__pycache__/account.cpython-312.pyc | Bin 0 -> 6363 bytes 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 scripts/__pycache__/account.cpython-312.pyc diff --git a/scripts/__pycache__/account.cpython-312.pyc b/scripts/__pycache__/account.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..741a4cd9f572356855e9a20c90dee817f8ee000e GIT binary patch literal 6363 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 literal 0 HcmV?d00001 From 20e1824c91a25b4d45d8ccf9186cce039f4ae82c Mon Sep 17 00:00:00 2001 From: chendelian <116870791@qq.com> Date: Mon, 23 Mar 2026 18:58:56 +0800 Subject: [PATCH 04/27] =?UTF-8?q?=E6=8F=90=E4=BA=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- SKILL.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/SKILL.md b/SKILL.md index dcf117d..c02bedc 100644 --- a/SKILL.md +++ b/SKILL.md @@ -1,7 +1,7 @@ --- name: account-manager description: 多平台多账号管理。管理各平台账号与Chrome Profile的对应关系,供publisher类Skill调用获取账号信息。 -version: 1.0.0 +version: 1.0.1 author: 深圳匠厂科技有限公司 metadata: openclaw: @@ -14,7 +14,7 @@ allowed-tools: ## 使用时机 -当用户发送以下内容时触发本Skill: +当用户发送以下内容的时候触发本Skill: - 说"初始化账号登录"、"登录搜狐账号"、"帮我登录账号" - 说"列出账号"、"查看所有账号"、"有哪些账号" - 说"添加账号"、"删除账号"、"获取账号信息" From 3a8279e6f156b19f945a757ca11dbb454053b171 Mon Sep 17 00:00:00 2001 From: chendelian <116870791@qq.com> Date: Tue, 24 Mar 2026 11:28:15 +0800 Subject: [PATCH 05/27] =?UTF-8?q?feat:=20=E6=B7=BB=E5=8A=A0=E8=87=AA?= =?UTF-8?q?=E5=8A=A8=E5=8F=91=E5=B8=83=E6=B5=81=E6=B0=B4=E7=BA=BF=E9=85=8D?= =?UTF-8?q?=E7=BD=AE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/release_skill.yaml | 70 ++++++++++++++++++++++++++++ 1 file changed, 70 insertions(+) create mode 100644 .github/workflows/release_skill.yaml diff --git a/.github/workflows/release_skill.yaml b/.github/workflows/release_skill.yaml new file mode 100644 index 0000000..f69734d --- /dev/null +++ b/.github/workflows/release_skill.yaml @@ -0,0 +1,70 @@ +name: Զ (԰-޼Ȩ) +on: + push: + tags: ["v*"] + +jobs: + build-and-deploy: + runs-on: ubuntu-latest + steps: + # 1. Դ + - uses: actions/checkout@v3 + + # 2. ׼ + - name: Setup Tools + run: pip install pyarmor requests python-frontmatter + + # 3. IJ衿PyArmor Դ + - name: Encrypt Source Code + run: | + mkdir -p dist/package + # ʹ PyArmor scripts Ŀ¼µ Python Դ + pyarmor gen -O dist/package scripts/*.py + # ˵ĵĿ¼ + cp SKILL.md dist/package/ + + # 4. ȡԪݲ + - name: Parse Metadata and Pack + id: build_task + run: | + python -c " + import frontmatter, os, json, shutil + post = frontmatter.load('SKILL.md') + metadata = post.metadata + slug = metadata['name'] + version = metadata['version'] + # ñ API ʹ + print(f'::set-output name=slug::{slug}') + print(f'::set-output name=version::{version}') + print(f'::set-output name=metadata::{json.dumps(metadata)}') + # յķ ZIP + shutil.make_archive(slug, 'zip', 'dist/package') + " + + # 5. ͬݿ¼ (Ӧӿ: /api/skill/update) + - name: Sync Database (V2.0) + run: | + python -c " + import requests, json + metadata = json.loads('${{ steps.build_task.outputs.metadata }}') + # Ͻӿ·Ϊ /api/skill/update + url = 'https://jc2009.com/api/skill/update' + res = requests.post(url, json=metadata) + print(f'DB Sync Result: {res.text}') + if res.status_code != 200: exit(1) + " + + # 6. ϴ (Ӧӿ: /api/upload) + - name: Upload Encrypted ZIP + run: | + SLUG=${{ steps.build_task.outputs.slug }} + VERSION=${{ steps.build_task.outputs.version }} + python -c " + import requests + url = 'https://jc2009.com/api/upload' + payload = {'plugin_name': '$SLUG', 'version': '$VERSION'} + with open(f'$SLUG.zip', 'rb') as f: + res = requests.post(url, data=payload, files={'file': f}) + print(f'Upload Result: {res.text}') + if res.status_code != 200: exit(1) + " From 721442315ff7ddf543227fb9c78eb7561efbf1ff Mon Sep 17 00:00:00 2001 From: chendelian <116870791@qq.com> Date: Tue, 24 Mar 2026 11:30:41 +0800 Subject: [PATCH 06/27] =?UTF-8?q?fix:=20=E4=BF=AE=E5=A4=8D=20YAML=20?= =?UTF-8?q?=E6=96=87=E4=BB=B6=E7=BC=96=E7=A0=81=E4=B8=BA=20UTF-8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/release_skill.yaml | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/.github/workflows/release_skill.yaml b/.github/workflows/release_skill.yaml index f69734d..5e5b4da 100644 --- a/.github/workflows/release_skill.yaml +++ b/.github/workflows/release_skill.yaml @@ -1,4 +1,4 @@ -name: Զ (԰-޼Ȩ) +name: 技能自动化发布 (测试版-无鉴权) on: push: tags: ["v*"] @@ -7,23 +7,23 @@ jobs: build-and-deploy: runs-on: ubuntu-latest steps: - # 1. Դ + # 1. 下载源码 - uses: actions/checkout@v3 - # 2. ׼ + # 2. 环境及工具准备 - name: Setup Tools run: pip install pyarmor requests python-frontmatter - # 3. IJ衿PyArmor Դ + # 3. 【核心步骤】PyArmor 源代码混淆加密 - name: Encrypt Source Code run: | mkdir -p dist/package - # ʹ PyArmor scripts Ŀ¼µ Python Դ + # 使用 PyArmor 加密 scripts 目录下的所有 Python 源码 pyarmor gen -O dist/package scripts/*.py - # ˵ĵĿ¼ + # 拷贝说明文档到发包目录 cp SKILL.md dist/package/ - # 4. ȡԪݲ + # 4. 提取元数据并物理打包 - name: Parse Metadata and Pack id: build_task run: | @@ -33,28 +33,28 @@ jobs: metadata = post.metadata slug = metadata['name'] version = metadata['version'] - # ñ API ʹ + # 设置变量供后续 API 调用使用 print(f'::set-output name=slug::{slug}') print(f'::set-output name=version::{version}') print(f'::set-output name=metadata::{json.dumps(metadata)}') - # յķ ZIP + # 制作最终的发布 ZIP 包 shutil.make_archive(slug, 'zip', 'dist/package') " - # 5. ͬݿ¼ (Ӧӿ: /api/skill/update) + # 5. 同步数据库记录 (对应接口: /api/skill/update) - name: Sync Database (V2.0) run: | python -c " import requests, json metadata = json.loads('${{ steps.build_task.outputs.metadata }}') - # Ͻӿ·Ϊ /api/skill/update + # 严谨修正:接口路径为 /api/skill/update url = 'https://jc2009.com/api/skill/update' res = requests.post(url, json=metadata) print(f'DB Sync Result: {res.text}') if res.status_code != 200: exit(1) " - # 6. ϴ (Ӧӿ: /api/upload) + # 6. 上传物理包 (对应接口: /api/upload) - name: Upload Encrypted ZIP run: | SLUG=${{ steps.build_task.outputs.slug }} From bad0846cc36547147b75a58b5d37805a71354776 Mon Sep 17 00:00:00 2001 From: chendelian <116870791@qq.com> Date: Tue, 24 Mar 2026 11:35:33 +0800 Subject: [PATCH 07/27] =?UTF-8?q?fix:=20=E4=BF=AE=E5=A4=8D=20pip=20?= =?UTF-8?q?=E5=AE=89=E8=A3=85=E9=99=90=E5=88=B6=E5=B9=B6=E5=8D=87=E7=BA=A7?= =?UTF-8?q?=E5=8F=98=E9=87=8F=E8=BE=93=E5=87=BA=E8=AF=AD=E6=B3=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/release_skill.yaml | 25 +++++++++++++------------ 1 file changed, 13 insertions(+), 12 deletions(-) diff --git a/.github/workflows/release_skill.yaml b/.github/workflows/release_skill.yaml index 5e5b4da..372443c 100644 --- a/.github/workflows/release_skill.yaml +++ b/.github/workflows/release_skill.yaml @@ -12,15 +12,14 @@ jobs: # 2. 环境及工具准备 - name: Setup Tools - run: pip install pyarmor requests python-frontmatter + # 修复:添加 --break-system-packages 参数 + run: pip install pyarmor requests python-frontmatter --break-system-packages # 3. 【核心步骤】PyArmor 源代码混淆加密 - name: Encrypt Source Code run: | mkdir -p dist/package - # 使用 PyArmor 加密 scripts 目录下的所有 Python 源码 pyarmor gen -O dist/package scripts/*.py - # 拷贝说明文档到发包目录 cp SKILL.md dist/package/ # 4. 提取元数据并物理打包 @@ -33,28 +32,30 @@ jobs: metadata = post.metadata slug = metadata['name'] version = metadata['version'] - # 设置变量供后续 API 调用使用 - print(f'::set-output name=slug::{slug}') - print(f'::set-output name=version::{version}') - print(f'::set-output name=metadata::{json.dumps(metadata)}') + # 修复:使用新的 $GITHUB_OUTPUT 方式设置变量,兼容性更好 + with open(os.environ['GITHUB_OUTPUT'], 'a') as f: + f.write(f'slug={slug}\n') + f.write(f'version={version}\n') + f.write(f'metadata={json.dumps(metadata)}\n') + # 制作最终的发布 ZIP 包 shutil.make_archive(slug, 'zip', 'dist/package') " - # 5. 同步数据库记录 (对应接口: /api/skill/update) + # 5. 同步数据库记录 - name: Sync Database (V2.0) run: | python -c " - import requests, json - metadata = json.loads('${{ steps.build_task.outputs.metadata }}') - # 严谨修正:接口路径为 /api/skill/update + import requests, json, os + # 修复:从环境变量读取 metadata + metadata = json.loads('''${{ steps.build_task.outputs.metadata }}''') url = 'https://jc2009.com/api/skill/update' res = requests.post(url, json=metadata) print(f'DB Sync Result: {res.text}') if res.status_code != 200: exit(1) " - # 6. 上传物理包 (对应接口: /api/upload) + # 6. 上传物理包 - name: Upload Encrypted ZIP run: | SLUG=${{ steps.build_task.outputs.slug }} From 9e7369a2af6be39d4003e19739685b2371f30569 Mon Sep 17 00:00:00 2001 From: chendelian <116870791@qq.com> Date: Tue, 24 Mar 2026 11:38:20 +0800 Subject: [PATCH 08/27] =?UTF-8?q?fix:=20=E4=BD=BF=E7=94=A8=E7=8E=AF?= =?UTF-8?q?=E5=A2=83=E5=8F=98=E9=87=8F=E4=BC=A0=E9=80=92=E6=95=B0=E6=8D=AE?= =?UTF-8?q?=E4=BB=A5=E4=BF=AE=E5=A4=8D=20Python=20=E8=A7=A3=E6=9E=90?= =?UTF-8?q?=E9=94=99=E8=AF=AF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/release_skill.yaml | 23 ++++++++++++++--------- 1 file changed, 14 insertions(+), 9 deletions(-) diff --git a/.github/workflows/release_skill.yaml b/.github/workflows/release_skill.yaml index 372443c..e70bfd2 100644 --- a/.github/workflows/release_skill.yaml +++ b/.github/workflows/release_skill.yaml @@ -12,7 +12,6 @@ jobs: # 2. 环境及工具准备 - name: Setup Tools - # 修复:添加 --break-system-packages 参数 run: pip install pyarmor requests python-frontmatter --break-system-packages # 3. 【核心步骤】PyArmor 源代码混淆加密 @@ -32,7 +31,7 @@ jobs: metadata = post.metadata slug = metadata['name'] version = metadata['version'] - # 修复:使用新的 $GITHUB_OUTPUT 方式设置变量,兼容性更好 + # 安全写入输出 with open(os.environ['GITHUB_OUTPUT'], 'a') as f: f.write(f'slug={slug}\n') f.write(f'version={version}\n') @@ -44,11 +43,14 @@ jobs: # 5. 同步数据库记录 - name: Sync Database (V2.0) + # 核心修复:通过 env 传递复杂 JSON 字符串 + env: + METADATA_JSON: ${{ steps.build_task.outputs.metadata }} run: | python -c " import requests, json, os - # 修复:从环境变量读取 metadata - metadata = json.loads('''${{ steps.build_task.outputs.metadata }}''') + # 从环境变量读取,不涉及脚本注入风险 + metadata = json.loads(os.environ['METADATA_JSON']) url = 'https://jc2009.com/api/skill/update' res = requests.post(url, json=metadata) print(f'DB Sync Result: {res.text}') @@ -57,14 +59,17 @@ jobs: # 6. 上传物理包 - name: Upload Encrypted ZIP + env: + SLUG: ${{ steps.build_task.outputs.slug }} + VERSION: ${{ steps.build_task.outputs.version }} run: | - SLUG=${{ steps.build_task.outputs.slug }} - VERSION=${{ steps.build_task.outputs.version }} python -c " - import requests + import requests, os url = 'https://jc2009.com/api/upload' - payload = {'plugin_name': '$SLUG', 'version': '$VERSION'} - with open(f'$SLUG.zip', 'rb') as f: + slug = os.environ['SLUG'] + version = os.environ['VERSION'] + payload = {'plugin_name': slug, 'version': version} + with open(f'{slug}.zip', 'rb') as f: res = requests.post(url, data=payload, files={'file': f}) print(f'Upload Result: {res.text}') if res.status_code != 200: exit(1) From 48c6bec254ebd9dd0da25cd1c04be76dd43da211 Mon Sep 17 00:00:00 2001 From: chendelian <116870791@qq.com> Date: Tue, 24 Mar 2026 11:47:12 +0800 Subject: [PATCH 09/27] =?UTF-8?q?chore:=20=E4=BD=BF=E7=94=A8=E6=B8=85?= =?UTF-8?q?=E5=8D=8E=E6=BA=90=E5=8A=A0=E9=80=9F=20pip=20=E5=AE=89=E8=A3=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/release_skill.yaml | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/.github/workflows/release_skill.yaml b/.github/workflows/release_skill.yaml index e70bfd2..dfd7d2c 100644 --- a/.github/workflows/release_skill.yaml +++ b/.github/workflows/release_skill.yaml @@ -12,7 +12,8 @@ jobs: # 2. 环境及工具准备 - name: Setup Tools - run: pip install pyarmor requests python-frontmatter --break-system-packages + # 优化:使用国内清华源加速安装速度 + run: pip install pyarmor requests python-frontmatter --break-system-packages -i https://pypi.tuna.tsinghua.edu.cn/simple # 3. 【核心步骤】PyArmor 源代码混淆加密 - name: Encrypt Source Code @@ -43,13 +44,11 @@ jobs: # 5. 同步数据库记录 - name: Sync Database (V2.0) - # 核心修复:通过 env 传递复杂 JSON 字符串 env: METADATA_JSON: ${{ steps.build_task.outputs.metadata }} run: | python -c " import requests, json, os - # 从环境变量读取,不涉及脚本注入风险 metadata = json.loads(os.environ['METADATA_JSON']) url = 'https://jc2009.com/api/skill/update' res = requests.post(url, json=metadata) From 16b3f50dac5c675f4587c0a18c69827cbf645eb2 Mon Sep 17 00:00:00 2001 From: chendelian <116870791@qq.com> Date: Tue, 24 Mar 2026 14:20:34 +0800 Subject: [PATCH 10/27] ci: set artifact_type=skill for release upload --- .github/workflows/release_skill.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/release_skill.yaml b/.github/workflows/release_skill.yaml index dfd7d2c..85468aa 100644 --- a/.github/workflows/release_skill.yaml +++ b/.github/workflows/release_skill.yaml @@ -67,7 +67,7 @@ jobs: url = 'https://jc2009.com/api/upload' slug = os.environ['SLUG'] version = os.environ['VERSION'] - payload = {'plugin_name': slug, 'version': version} + payload = {'plugin_name': slug, 'version': version, 'artifact_type': 'skill'} with open(f'{slug}.zip', 'rb') as f: res = requests.post(url, data=payload, files={'file': f}) print(f'Upload Result: {res.text}') From 92a9d61007d4dad83d944d499f84db871d5cf21c Mon Sep 17 00:00:00 2001 From: chendelian <116870791@qq.com> Date: Tue, 24 Mar 2026 15:09:02 +0800 Subject: [PATCH 11/27] ci: use mirrored checkout action Switch workflow checkout step to internal Gitea mirrored actions-checkout to reduce external GitHub dependency and speed up setup. Made-with: Cursor --- .github/workflows/release_skill.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/release_skill.yaml b/.github/workflows/release_skill.yaml index 85468aa..05ea84f 100644 --- a/.github/workflows/release_skill.yaml +++ b/.github/workflows/release_skill.yaml @@ -8,7 +8,7 @@ jobs: runs-on: ubuntu-latest steps: # 1. 下载源码 - - uses: actions/checkout@v3 + - uses: http://120.25.191.12:3000/admin/actions-checkout@v4 # 2. 环境及工具准备 - name: Setup Tools From 7759e0b37669600f9696f3c15d9ebd0c95036e63 Mon Sep 17 00:00:00 2001 From: chendelian <116870791@qq.com> Date: Tue, 24 Mar 2026 15:55:39 +0800 Subject: [PATCH 12/27] fix: add required openclaw metadata for skill sync Include category and pricing-related openclaw metadata in SKILL frontmatter so /api/skill/update validation can pass during CI sync. Made-with: Cursor --- SKILL.md | 34 +++++----------------------------- 1 file changed, 5 insertions(+), 29 deletions(-) diff --git a/SKILL.md b/SKILL.md index c02bedc..c5c499f 100644 --- a/SKILL.md +++ b/SKILL.md @@ -6,6 +6,10 @@ author: 深圳匠厂科技有限公司 metadata: openclaw: emoji: "👤" + category: "通用" + skill_type: 1 + monthly_price: 0 + yearly_price: 0 allowed-tools: - bash --- @@ -23,32 +27,4 @@ allowed-tools: ### 列出某平台所有账号 ```bash -python3 {baseDir}/scripts/account.py list -``` - -### 获取某个账号详情 -```bash -python3 {baseDir}/scripts/account.py get -``` - -### 初始化账号登录(首次使用,打开浏览器手动登录) -```bash -python3 {baseDir}/scripts/account.py login -``` - -## 输出格式 - -- list:逐行返回账号ID和名称 -- get:返回账号完整信息 -- login:打开浏览器后提示用户手动完成登录 - -## 错误处理 - -- 账号不存在:提示"未找到该账号,请检查账号ID是否正确" -- 平台无账号:提示"该平台暂无账号,请先在accounts.json中添加" - -## 注意事项 - -- 账号档案存储在 `accounts.json`,新增账号需手动编辑该文件 -- Chrome Profile目录首次登录后自动创建,登录态永久保存 -- 每个账号只需初始化登录一次 \ No newline at end of file +python3 {baseDir}/scripts/account.py list \ No newline at end of file From bea22d5e9eaeb6c7f39a45c08353c3e08e0f3078 Mon Sep 17 00:00:00 2001 From: chendelian <116870791@qq.com> Date: Tue, 24 Mar 2026 16:14:06 +0800 Subject: [PATCH 13/27] ci: enforce tag-based version and strict sync checks Use Git tag as release version, validate SKILL.md version consistency, and fail workflow when API business code is not success. --- .github/workflows/release_skill.yaml | 40 +++++++++++++++++++++++++--- SKILL.md | 2 +- 2 files changed, 37 insertions(+), 5 deletions(-) diff --git a/.github/workflows/release_skill.yaml b/.github/workflows/release_skill.yaml index 05ea84f..b6193fc 100644 --- a/.github/workflows/release_skill.yaml +++ b/.github/workflows/release_skill.yaml @@ -30,8 +30,26 @@ jobs: import frontmatter, os, json, shutil post = frontmatter.load('SKILL.md') metadata = post.metadata - slug = metadata['name'] - version = metadata['version'] + # slug 优先从 openclaw.slug 读取,兼容历史用 name + openclaw_meta = metadata.get('metadata', {}).get('openclaw', {}) + slug = (openclaw_meta.get('slug') or metadata.get('slug') or metadata.get('name') or '').strip() + if not slug: + raise Exception('SKILL.md 缺少 slug/name,无法构建发布包') + + # 以 Git Tag 为准(严格模式) + ref_name = (os.environ.get('GITHUB_REF_NAME') or '').strip() + if not ref_name: + ref_name = (os.environ.get('GITHUB_REF') or '').strip().split('/')[-1] + if not ref_name.startswith('v'): + raise Exception(f'非法标签: {ref_name},要求以 v 开头,例如 v1.0.10') + + version = ref_name.lstrip('v') + skill_version = str(metadata.get('version') or '').strip() + if skill_version != version: + raise Exception(f'SKILL.md 版本({skill_version})与 Git Tag 版本({version})不一致') + + # 统一覆盖,确保后续输出与上传完全一致 + metadata['version'] = version # 安全写入输出 with open(os.environ['GITHUB_OUTPUT'], 'a') as f: f.write(f'slug={slug}\n') @@ -53,7 +71,14 @@ jobs: url = 'https://jc2009.com/api/skill/update' res = requests.post(url, json=metadata) print(f'DB Sync Result: {res.text}') - if res.status_code != 200: exit(1) + if res.status_code != 200: + exit(1) + try: + payload = res.json() + except Exception: + exit(1) + if payload.get('code') != 200: + exit(1) " # 6. 上传物理包 @@ -71,5 +96,12 @@ jobs: with open(f'{slug}.zip', 'rb') as f: res = requests.post(url, data=payload, files={'file': f}) print(f'Upload Result: {res.text}') - if res.status_code != 200: exit(1) + if res.status_code != 200: + exit(1) + try: + payload = res.json() + except Exception: + exit(1) + if payload.get('code') != 200: + exit(1) " diff --git a/SKILL.md b/SKILL.md index c5c499f..5ae6188 100644 --- a/SKILL.md +++ b/SKILL.md @@ -14,7 +14,7 @@ allowed-tools: - bash --- -# 账号管理器 +# 账号管理 ## 使用时机 From 2e3ac2cfe730d539c669d0f3fb9937039b13280f Mon Sep 17 00:00:00 2001 From: chendelian <116870791@qq.com> Date: Tue, 24 Mar 2026 16:24:09 +0800 Subject: [PATCH 14/27] feat: separate skill display name from slug Set SKILL name to Chinese display text and add openclaw.slug for stable machine identifier, so database records can keep readable names while preserving stable install path keys. Made-with: Cursor --- SKILL.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/SKILL.md b/SKILL.md index 5ae6188..8c42496 100644 --- a/SKILL.md +++ b/SKILL.md @@ -1,10 +1,11 @@ --- -name: account-manager +name: 账号管理 description: 多平台多账号管理。管理各平台账号与Chrome Profile的对应关系,供publisher类Skill调用获取账号信息。 version: 1.0.1 author: 深圳匠厂科技有限公司 metadata: openclaw: + slug: account-manager emoji: "👤" category: "通用" skill_type: 1 From cd131697d57931e5dc29441957e281571bb3b017 Mon Sep 17 00:00:00 2001 From: chendelian <116870791@qq.com> Date: Tue, 24 Mar 2026 16:42:34 +0800 Subject: [PATCH 15/27] =?UTF-8?q?=E4=BF=AE=E6=94=B9=E7=89=88=E6=9C=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/release_skill.yaml | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/.github/workflows/release_skill.yaml b/.github/workflows/release_skill.yaml index b6193fc..be669a1 100644 --- a/.github/workflows/release_skill.yaml +++ b/.github/workflows/release_skill.yaml @@ -30,13 +30,14 @@ jobs: import frontmatter, os, json, shutil post = frontmatter.load('SKILL.md') metadata = post.metadata + # slug 优先从 openclaw.slug 读取,兼容历史用 name openclaw_meta = metadata.get('metadata', {}).get('openclaw', {}) slug = (openclaw_meta.get('slug') or metadata.get('slug') or metadata.get('name') or '').strip() if not slug: raise Exception('SKILL.md 缺少 slug/name,无法构建发布包') - # 以 Git Tag 为准(严格模式) + # 以 Git Tag 为准(自动模式) ref_name = (os.environ.get('GITHUB_REF_NAME') or '').strip() if not ref_name: ref_name = (os.environ.get('GITHUB_REF') or '').strip().split('/')[-1] @@ -44,18 +45,21 @@ jobs: raise Exception(f'非法标签: {ref_name},要求以 v 开头,例如 v1.0.10') version = ref_name.lstrip('v') + + # 自动模式:SKILL.md 与 tag 不一致时仅告警,不阻塞发布 skill_version = str(metadata.get('version') or '').strip() - if skill_version != version: - raise Exception(f'SKILL.md 版本({skill_version})与 Git Tag 版本({version})不一致') + if skill_version and skill_version != version: + print(f'WARNING: SKILL.md version({skill_version}) != tag version({version}), using tag version') # 统一覆盖,确保后续输出与上传完全一致 metadata['version'] = version + # 安全写入输出 with open(os.environ['GITHUB_OUTPUT'], 'a') as f: f.write(f'slug={slug}\n') f.write(f'version={version}\n') - f.write(f'metadata={json.dumps(metadata)}\n') - + f.write(f'metadata={json.dumps(metadata, ensure_ascii=False)}\n') + # 制作最终的发布 ZIP 包 shutil.make_archive(slug, 'zip', 'dist/package') " @@ -104,4 +108,4 @@ jobs: exit(1) if payload.get('code') != 200: exit(1) - " + " \ No newline at end of file From 3b6f15ecbbd25e5354b027b6687847ae6d470305 Mon Sep 17 00:00:00 2001 From: chendelian <116870791@qq.com> Date: Tue, 24 Mar 2026 18:05:25 +0800 Subject: [PATCH 16/27] chore: auto release commit (2026-03-24 18:05:25) --- release.ps1 | 209 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 209 insertions(+) create mode 100644 release.ps1 diff --git a/release.ps1 b/release.ps1 new file mode 100644 index 0000000..f81bbfb --- /dev/null +++ b/release.ps1 @@ -0,0 +1,209 @@ +<# +.SYNOPSIS + One-command release script for skill repos. + +.DESCRIPTION + - Optional auto-commit + - Push current branch + - Auto-increment semantic tag (vX.Y.Z) + - Create & push tag + - Fail fast on unsafe states + +.EXAMPLES + # Safe mode (recommended): requires clean working tree + .\release.ps1 + + # Auto commit tracked/untracked changes then release + .\release.ps1 -AutoCommit -CommitMessage "chore: update skill config" + + # Dry run (show what would happen) + .\release.ps1 -DryRun + + # Custom tag prefix + .\release.ps1 -Prefix "v" -Message "正式发布" + +.NOTES + Requires: git, PowerShell 5+ +#> + +[CmdletBinding()] +param( + [string]$Prefix = "v", + [string]$Message = "正式发布", + [switch]$AutoCommit, + [switch]$RequireClean, + [string]$CommitMessage, + [switch]$DryRun +) + +Set-StrictMode -Version Latest +$ErrorActionPreference = "Stop" + +function Invoke-Git { + param([Parameter(Mandatory = $true)][string]$Args) + Write-Host ">> git $Args" -ForegroundColor DarkGray + & cmd /c "git $Args" + if ($LASTEXITCODE -ne 0) { + throw "git command failed: git $Args" + } +} + + +function Get-GitOutput { + param([Parameter(Mandatory = $true)][string]$Args) + $output = & cmd /c "git $Args" 2>$null + if ($LASTEXITCODE -ne 0) { + throw "git command failed: git $Args" + } + return @($output) +} + +function Test-Repo { + & git rev-parse --is-inside-work-tree *> $null + return ($LASTEXITCODE -eq 0) +} + +function Get-CurrentBranch { + $b = (Get-GitOutput "branch --show-current" | Select-Object -First 1).Trim() + return $b +} + + +function Get-StatusPorcelain { + $lines = @(Get-GitOutput "status --porcelain") + return $lines +} + +function Parse-SemVerTag { + param( + [string]$Tag, + [string]$TagPrefix + ) + $escaped = [regex]::Escape($TagPrefix) + $m = [regex]::Match($Tag, "^${escaped}(\d+)\.(\d+)\.(\d+)$") + if (-not $m.Success) { return $null } + + return [pscustomobject]@{ + Raw = $Tag + Major = [int]$m.Groups[1].Value + Minor = [int]$m.Groups[2].Value + Patch = [int]$m.Groups[3].Value + } +} + +function Get-NextTag { + param([string]$TagPrefix) + + $tags = Get-GitOutput "tag --list" + $parsed = @() + + foreach ($t in $tags) { + $t = $t.Trim() + if (-not $t) { continue } + $obj = Parse-SemVerTag -Tag $t -TagPrefix $TagPrefix + if ($null -ne $obj) { $parsed += $obj } + } + + if ($parsed.Count -eq 0) { + return "${TagPrefix}1.0.1" + } + + $latest = $parsed | Sort-Object Major, Minor, Patch | Select-Object -Last 1 + return "$TagPrefix$($latest.Major).$($latest.Minor).$([int]$latest.Patch + 1)" +} + + +function Ensure-CleanOrAutoCommit { + param( + [switch]$DoAutoCommit, + [switch]$NeedClean, + [string]$Msg + ) + + $status = @(Get-StatusPorcelain) + if ($status.Length -eq 0) { return } + + if ($NeedClean) { + Write-Host "Working tree is not clean and -RequireClean is enabled." -ForegroundColor Yellow + & git status --short + throw "Abort: dirty working tree." + } + + # 默认一键发布:有改动就自动提交;也可用 -AutoCommit 显式开启 + $commitMsg = $Msg + if ([string]::IsNullOrWhiteSpace($commitMsg)) { + $commitMsg = "chore: auto release commit ($(Get-Date -Format 'yyyy-MM-dd HH:mm:ss'))" + } + + if (-not $DoAutoCommit) { + Write-Host "Detected uncommitted changes, auto-committing before release..." -ForegroundColor Yellow + } + + Invoke-Git "add -A" + Invoke-Git "commit -m `"$commitMsg`"" +} + + +try { + Write-Host "=== Release Script Start ===" -ForegroundColor Cyan + + if (-not (Test-Repo)) { + throw "Current directory is not a git repository." + } + + $branch = Get-CurrentBranch + if ([string]::IsNullOrWhiteSpace($branch)) { + throw "Unable to determine current branch." + } + + if ($branch -notin @("main", "master")) { + throw "Current branch is '$branch'. Release is only allowed from main/master." + } + + Invoke-Git "fetch --tags --prune origin" + + Ensure-CleanOrAutoCommit -DoAutoCommit:$AutoCommit -NeedClean:$RequireClean -Msg $CommitMessage + + $upstream = (& git rev-parse --abbrev-ref --symbolic-full-name "@{u}" 2>$null) + $hasUpstream = ($LASTEXITCODE -eq 0) + + if ($DryRun) { + if ($hasUpstream) { + Write-Host "[DryRun] Would run: git push" -ForegroundColor Yellow + } else { + Write-Host "[DryRun] Would run: git push -u origin $branch" -ForegroundColor Yellow + } + } else { + if ($hasUpstream) { + Invoke-Git "push" + } else { + Invoke-Git "push -u origin $branch" + } + } + + $nextTag = Get-NextTag -TagPrefix $Prefix + Write-Host "Next tag: $nextTag" -ForegroundColor Green + + $existing = Get-GitOutput "tag --list `"$nextTag`"" + if ($existing.Count -gt 0) { + throw "Tag already exists: $nextTag" + } + + if ($DryRun) { + Write-Host "[DryRun] Would run: git tag -a $nextTag -m `"$Message`"" -ForegroundColor Yellow + Write-Host "[DryRun] Would run: git push origin $nextTag" -ForegroundColor Yellow + Write-Host "=== DryRun Complete ===" -ForegroundColor Cyan + exit 0 + } + + Invoke-Git "tag -a $nextTag -m `"$Message`"" + Invoke-Git "push origin $nextTag" + + Write-Host "Release success: $nextTag" -ForegroundColor Green + Write-Host "=== Release Script Done ===" -ForegroundColor Cyan + exit 0 +} +catch { + Write-Host "Release failed: $($_.Exception.Message)" -ForegroundColor Red + exit 1 +} From d54a69cba87ea02ba0b2e8309e5e4b26342f18a8 Mon Sep 17 00:00:00 2001 From: chendelian <116870791@qq.com> Date: Tue, 24 Mar 2026 18:06:10 +0800 Subject: [PATCH 17/27] chore: auto release commit (2026-03-24 18:06:10) --- release.ps1 | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/release.ps1 b/release.ps1 index f81bbfb..ff48aab 100644 --- a/release.ps1 +++ b/release.ps1 @@ -184,8 +184,8 @@ try { $nextTag = Get-NextTag -TagPrefix $Prefix Write-Host "Next tag: $nextTag" -ForegroundColor Green - $existing = Get-GitOutput "tag --list `"$nextTag`"" - if ($existing.Count -gt 0) { + $existing = @(Get-GitOutput "tag --list `"$nextTag`"") + if ($existing.Length -gt 0) { throw "Tag already exists: $nextTag" } From 157f51c7652515b1bed4862f30a8967d291c88c8 Mon Sep 17 00:00:00 2001 From: chendelian <116870791@qq.com> Date: Tue, 24 Mar 2026 18:07:44 +0800 Subject: [PATCH 18/27] chore: auto release commit (2026-03-24 18:07:44) --- release.ps1 | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/release.ps1 b/release.ps1 index ff48aab..9a3aa41 100644 --- a/release.ps1 +++ b/release.ps1 @@ -117,6 +117,7 @@ function Ensure-CleanOrAutoCommit { param( [switch]$DoAutoCommit, [switch]$NeedClean, + [switch]$IsDryRun, [string]$Msg ) @@ -139,6 +140,12 @@ function Ensure-CleanOrAutoCommit { Write-Host "Detected uncommitted changes, auto-committing before release..." -ForegroundColor Yellow } + if ($IsDryRun) { + Write-Host "[DryRun] Would run: git add -A" -ForegroundColor Yellow + Write-Host "[DryRun] Would run: git commit -m `"$commitMsg`"" -ForegroundColor Yellow + return + } + Invoke-Git "add -A" Invoke-Git "commit -m `"$commitMsg`"" } @@ -162,7 +169,7 @@ try { Invoke-Git "fetch --tags --prune origin" - Ensure-CleanOrAutoCommit -DoAutoCommit:$AutoCommit -NeedClean:$RequireClean -Msg $CommitMessage + Ensure-CleanOrAutoCommit -DoAutoCommit:$AutoCommit -NeedClean:$RequireClean -IsDryRun:$DryRun -Msg $CommitMessage $upstream = (& git rev-parse --abbrev-ref --symbolic-full-name "@{u}" 2>$null) $hasUpstream = ($LASTEXITCODE -eq 0) From edaac8aa45f8cb237eb884f2a66e51cc83f2fad6 Mon Sep 17 00:00:00 2001 From: chendelian <116870791@qq.com> Date: Tue, 24 Mar 2026 18:22:46 +0800 Subject: [PATCH 19/27] chore: auto release commit (2026-03-24 18:22:46) --- .github/workflows/release_skill.yaml | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/.github/workflows/release_skill.yaml b/.github/workflows/release_skill.yaml index be669a1..f27ccc6 100644 --- a/.github/workflows/release_skill.yaml +++ b/.github/workflows/release_skill.yaml @@ -108,4 +108,31 @@ jobs: exit(1) if payload.get('code') != 200: exit(1) + " + + # 7. 清理旧版本(只保留最新1个) + - name: Prune Old Versions + env: + SLUG: ${{ steps.build_task.outputs.slug }} + VERSION: ${{ steps.build_task.outputs.version }} + run: | + python -c " + import requests, os + url = 'https://jc2009.com/api/artifacts/prune-old-versions' + payload = { + 'name': os.environ['SLUG'], + 'artifact_type': 'skill', + 'keep_count': 1, + 'protect_version': os.environ['VERSION'] + } + res = requests.post(url, json=payload) + print(f'Prune Result: {res.text}') + if res.status_code != 200: + exit(1) + try: + body = res.json() + except Exception: + exit(1) + if body.get('code') != 200: + exit(1) " \ No newline at end of file From 973547b6fb55ce5d499a60655af295254acef56c Mon Sep 17 00:00:00 2001 From: chendelian <116870791@qq.com> Date: Mon, 30 Mar 2026 19:10:46 +0800 Subject: [PATCH 20/27] chore: auto release commit (2026-03-30 19:10:46) --- .github/workflows/release_skill.yaml | 140 +----------------- release.ps1 | 209 +-------------------------- 2 files changed, 15 insertions(+), 334 deletions(-) diff --git a/.github/workflows/release_skill.yaml b/.github/workflows/release_skill.yaml index f27ccc6..dd1cca2 100644 --- a/.github/workflows/release_skill.yaml +++ b/.github/workflows/release_skill.yaml @@ -1,138 +1,12 @@ -name: 技能自动化发布 (测试版-无鉴权) +name: 技能自动化发布 on: push: tags: ["v*"] jobs: - build-and-deploy: - runs-on: ubuntu-latest - steps: - # 1. 下载源码 - - uses: http://120.25.191.12:3000/admin/actions-checkout@v4 - - # 2. 环境及工具准备 - - name: Setup Tools - # 优化:使用国内清华源加速安装速度 - run: pip install pyarmor requests python-frontmatter --break-system-packages -i https://pypi.tuna.tsinghua.edu.cn/simple - - # 3. 【核心步骤】PyArmor 源代码混淆加密 - - name: Encrypt Source Code - run: | - mkdir -p dist/package - pyarmor gen -O dist/package scripts/*.py - cp SKILL.md dist/package/ - - # 4. 提取元数据并物理打包 - - name: Parse Metadata and Pack - id: build_task - run: | - python -c " - import frontmatter, os, json, shutil - post = frontmatter.load('SKILL.md') - metadata = post.metadata - - # slug 优先从 openclaw.slug 读取,兼容历史用 name - openclaw_meta = metadata.get('metadata', {}).get('openclaw', {}) - slug = (openclaw_meta.get('slug') or metadata.get('slug') or metadata.get('name') or '').strip() - if not slug: - raise Exception('SKILL.md 缺少 slug/name,无法构建发布包') - - # 以 Git Tag 为准(自动模式) - ref_name = (os.environ.get('GITHUB_REF_NAME') or '').strip() - if not ref_name: - ref_name = (os.environ.get('GITHUB_REF') or '').strip().split('/')[-1] - if not ref_name.startswith('v'): - raise Exception(f'非法标签: {ref_name},要求以 v 开头,例如 v1.0.10') - - version = ref_name.lstrip('v') - - # 自动模式:SKILL.md 与 tag 不一致时仅告警,不阻塞发布 - skill_version = str(metadata.get('version') or '').strip() - if skill_version and skill_version != version: - print(f'WARNING: SKILL.md version({skill_version}) != tag version({version}), using tag version') - - # 统一覆盖,确保后续输出与上传完全一致 - metadata['version'] = version - - # 安全写入输出 - with open(os.environ['GITHUB_OUTPUT'], 'a') as f: - f.write(f'slug={slug}\n') - f.write(f'version={version}\n') - f.write(f'metadata={json.dumps(metadata, ensure_ascii=False)}\n') - - # 制作最终的发布 ZIP 包 - shutil.make_archive(slug, 'zip', 'dist/package') - " - - # 5. 同步数据库记录 - - name: Sync Database (V2.0) - env: - METADATA_JSON: ${{ steps.build_task.outputs.metadata }} - run: | - python -c " - import requests, json, os - metadata = json.loads(os.environ['METADATA_JSON']) - url = 'https://jc2009.com/api/skill/update' - res = requests.post(url, json=metadata) - print(f'DB Sync Result: {res.text}') - if res.status_code != 200: - exit(1) - try: - payload = res.json() - except Exception: - exit(1) - if payload.get('code') != 200: - exit(1) - " - - # 6. 上传物理包 - - name: Upload Encrypted ZIP - env: - SLUG: ${{ steps.build_task.outputs.slug }} - VERSION: ${{ steps.build_task.outputs.version }} - run: | - python -c " - import requests, os - url = 'https://jc2009.com/api/upload' - slug = os.environ['SLUG'] - version = os.environ['VERSION'] - payload = {'plugin_name': slug, 'version': version, 'artifact_type': 'skill'} - with open(f'{slug}.zip', 'rb') as f: - res = requests.post(url, data=payload, files={'file': f}) - print(f'Upload Result: {res.text}') - if res.status_code != 200: - exit(1) - try: - payload = res.json() - except Exception: - exit(1) - if payload.get('code') != 200: - exit(1) - " - - # 7. 清理旧版本(只保留最新1个) - - name: Prune Old Versions - env: - SLUG: ${{ steps.build_task.outputs.slug }} - VERSION: ${{ steps.build_task.outputs.version }} - run: | - python -c " - import requests, os - url = 'https://jc2009.com/api/artifacts/prune-old-versions' - payload = { - 'name': os.environ['SLUG'], - 'artifact_type': 'skill', - 'keep_count': 1, - 'protect_version': os.environ['VERSION'] - } - res = requests.post(url, json=payload) - print(f'Prune Result: {res.text}') - if res.status_code != 200: - exit(1) - try: - body = res.json() - except Exception: - exit(1) - if body.get('code') != 200: - exit(1) - " \ No newline at end of file + release: + uses: admin/jiangchang-platform-kit/.github/workflows/reusable-release-skill.yaml@main + with: + artifact_platform: windows + pyarmor_platform: windows.x86_64 + include_readme_md: false \ No newline at end of file diff --git a/release.ps1 b/release.ps1 index 9a3aa41..639081a 100644 --- a/release.ps1 +++ b/release.ps1 @@ -1,31 +1,3 @@ -<# -.SYNOPSIS - One-command release script for skill repos. - -.DESCRIPTION - - Optional auto-commit - - Push current branch - - Auto-increment semantic tag (vX.Y.Z) - - Create & push tag - - Fail fast on unsafe states - -.EXAMPLES - # Safe mode (recommended): requires clean working tree - .\release.ps1 - - # Auto commit tracked/untracked changes then release - .\release.ps1 -AutoCommit -CommitMessage "chore: update skill config" - - # Dry run (show what would happen) - .\release.ps1 -DryRun - - # Custom tag prefix - .\release.ps1 -Prefix "v" -Message "正式发布" - -.NOTES - Requires: git, PowerShell 5+ -#> - [CmdletBinding()] param( [string]$Prefix = "v", @@ -39,178 +11,13 @@ param( Set-StrictMode -Version Latest $ErrorActionPreference = "Stop" -function Invoke-Git { - param([Parameter(Mandatory = $true)][string]$Args) - Write-Host ">> git $Args" -ForegroundColor DarkGray - & cmd /c "git $Args" - if ($LASTEXITCODE -ne 0) { - throw "git command failed: git $Args" - } +$scriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path +$sharedScript = Join-Path $scriptDir "..\jiangchang-platform-kit\tools\release.ps1" +$sharedScript = [System.IO.Path]::GetFullPath($sharedScript) + +if (-not (Test-Path $sharedScript)) { + throw "Shared release script not found: $sharedScript" } - -function Get-GitOutput { - param([Parameter(Mandatory = $true)][string]$Args) - $output = & cmd /c "git $Args" 2>$null - if ($LASTEXITCODE -ne 0) { - throw "git command failed: git $Args" - } - return @($output) -} - -function Test-Repo { - & git rev-parse --is-inside-work-tree *> $null - return ($LASTEXITCODE -eq 0) -} - -function Get-CurrentBranch { - $b = (Get-GitOutput "branch --show-current" | Select-Object -First 1).Trim() - return $b -} - - -function Get-StatusPorcelain { - $lines = @(Get-GitOutput "status --porcelain") - return $lines -} - -function Parse-SemVerTag { - param( - [string]$Tag, - [string]$TagPrefix - ) - $escaped = [regex]::Escape($TagPrefix) - $m = [regex]::Match($Tag, "^${escaped}(\d+)\.(\d+)\.(\d+)$") - if (-not $m.Success) { return $null } - - return [pscustomobject]@{ - Raw = $Tag - Major = [int]$m.Groups[1].Value - Minor = [int]$m.Groups[2].Value - Patch = [int]$m.Groups[3].Value - } -} - -function Get-NextTag { - param([string]$TagPrefix) - - $tags = Get-GitOutput "tag --list" - $parsed = @() - - foreach ($t in $tags) { - $t = $t.Trim() - if (-not $t) { continue } - $obj = Parse-SemVerTag -Tag $t -TagPrefix $TagPrefix - if ($null -ne $obj) { $parsed += $obj } - } - - if ($parsed.Count -eq 0) { - return "${TagPrefix}1.0.1" - } - - $latest = $parsed | Sort-Object Major, Minor, Patch | Select-Object -Last 1 - return "$TagPrefix$($latest.Major).$($latest.Minor).$([int]$latest.Patch + 1)" -} - - -function Ensure-CleanOrAutoCommit { - param( - [switch]$DoAutoCommit, - [switch]$NeedClean, - [switch]$IsDryRun, - [string]$Msg - ) - - $status = @(Get-StatusPorcelain) - if ($status.Length -eq 0) { return } - - if ($NeedClean) { - Write-Host "Working tree is not clean and -RequireClean is enabled." -ForegroundColor Yellow - & git status --short - throw "Abort: dirty working tree." - } - - # 默认一键发布:有改动就自动提交;也可用 -AutoCommit 显式开启 - $commitMsg = $Msg - if ([string]::IsNullOrWhiteSpace($commitMsg)) { - $commitMsg = "chore: auto release commit ($(Get-Date -Format 'yyyy-MM-dd HH:mm:ss'))" - } - - if (-not $DoAutoCommit) { - Write-Host "Detected uncommitted changes, auto-committing before release..." -ForegroundColor Yellow - } - - if ($IsDryRun) { - Write-Host "[DryRun] Would run: git add -A" -ForegroundColor Yellow - Write-Host "[DryRun] Would run: git commit -m `"$commitMsg`"" -ForegroundColor Yellow - return - } - - Invoke-Git "add -A" - Invoke-Git "commit -m `"$commitMsg`"" -} - - -try { - Write-Host "=== Release Script Start ===" -ForegroundColor Cyan - - if (-not (Test-Repo)) { - throw "Current directory is not a git repository." - } - - $branch = Get-CurrentBranch - if ([string]::IsNullOrWhiteSpace($branch)) { - throw "Unable to determine current branch." - } - - if ($branch -notin @("main", "master")) { - throw "Current branch is '$branch'. Release is only allowed from main/master." - } - - Invoke-Git "fetch --tags --prune origin" - - Ensure-CleanOrAutoCommit -DoAutoCommit:$AutoCommit -NeedClean:$RequireClean -IsDryRun:$DryRun -Msg $CommitMessage - - $upstream = (& git rev-parse --abbrev-ref --symbolic-full-name "@{u}" 2>$null) - $hasUpstream = ($LASTEXITCODE -eq 0) - - if ($DryRun) { - if ($hasUpstream) { - Write-Host "[DryRun] Would run: git push" -ForegroundColor Yellow - } else { - Write-Host "[DryRun] Would run: git push -u origin $branch" -ForegroundColor Yellow - } - } else { - if ($hasUpstream) { - Invoke-Git "push" - } else { - Invoke-Git "push -u origin $branch" - } - } - - $nextTag = Get-NextTag -TagPrefix $Prefix - Write-Host "Next tag: $nextTag" -ForegroundColor Green - - $existing = @(Get-GitOutput "tag --list `"$nextTag`"") - if ($existing.Length -gt 0) { - throw "Tag already exists: $nextTag" - } - - if ($DryRun) { - Write-Host "[DryRun] Would run: git tag -a $nextTag -m `"$Message`"" -ForegroundColor Yellow - Write-Host "[DryRun] Would run: git push origin $nextTag" -ForegroundColor Yellow - Write-Host "=== DryRun Complete ===" -ForegroundColor Cyan - exit 0 - } - - Invoke-Git "tag -a $nextTag -m `"$Message`"" - Invoke-Git "push origin $nextTag" - - Write-Host "Release success: $nextTag" -ForegroundColor Green - Write-Host "=== Release Script Done ===" -ForegroundColor Cyan - exit 0 -} -catch { - Write-Host "Release failed: $($_.Exception.Message)" -ForegroundColor Red - exit 1 -} +& $sharedScript @PSBoundParameters +exit $LASTEXITCODE From 66d3801e43977268a35c5e0903075bb8fc1986f2 Mon Sep 17 00:00:00 2001 From: chendelian <116870791@qq.com> Date: Mon, 30 Mar 2026 19:25:21 +0800 Subject: [PATCH 21/27] chore: auto release commit (2026-03-30 19:25:21) --- .github/workflows/release_skill.yaml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/.github/workflows/release_skill.yaml b/.github/workflows/release_skill.yaml index dd1cca2..229e053 100644 --- a/.github/workflows/release_skill.yaml +++ b/.github/workflows/release_skill.yaml @@ -8,5 +8,4 @@ jobs: uses: admin/jiangchang-platform-kit/.github/workflows/reusable-release-skill.yaml@main with: artifact_platform: windows - pyarmor_platform: windows.x86_64 - include_readme_md: false \ No newline at end of file + pyarmor_platform: windows.x86_64 \ No newline at end of file From 01d9c82f800d7ad0d645ff1069a3e7277d9754d6 Mon Sep 17 00:00:00 2001 From: chendelian <116870791@qq.com> Date: Tue, 31 Mar 2026 11:50:56 +0800 Subject: [PATCH 22/27] chore: auto release commit (2026-03-31 11:50:55) --- SKILL.md | 3 - scripts/account.py | 210 ++++++++++++++++++++++++++++++++++++++------- 2 files changed, 180 insertions(+), 33 deletions(-) diff --git a/SKILL.md b/SKILL.md index 8c42496..9e5ad1b 100644 --- a/SKILL.md +++ b/SKILL.md @@ -8,9 +8,6 @@ metadata: slug: account-manager emoji: "👤" category: "通用" - skill_type: 1 - monthly_price: 0 - yearly_price: 0 allowed-tools: - bash --- diff --git a/scripts/account.py b/scripts/account.py index 17a79de..4b68772 100644 --- a/scripts/account.py +++ b/scripts/account.py @@ -1,7 +1,9 @@ import sys import json import os +import sqlite3 import subprocess +from datetime import datetime # Windows GBK 编码兼容修复 if sys.platform == "win32": @@ -9,7 +11,8 @@ if sys.platform == "win32": sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8', errors='replace') sys.stderr = io.TextIOWrapper(sys.stderr.buffer, encoding='utf-8', errors='replace') BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) -ACCOUNTS_FILE = os.path.join(BASE_DIR, "accounts.json") +SKILL_SLUG = "account-manager" +LEGACY_ACCOUNTS_FILE = os.path.join(BASE_DIR, "accounts.json") PLATFORM_URLS = { # 自媒体/图文平台 @@ -26,50 +29,197 @@ PLATFORM_URLS = { "yuanbao": "https://yuanbao.tencent.com" } -def load_accounts(): - with open(ACCOUNTS_FILE, "r", encoding="utf-8") as f: - return json.load(f) + +def get_data_root(): + return (os.getenv("JIANGCHANG_DATA_ROOT") or os.path.join(os.path.expanduser("~"), ".jiangchang-data")).strip() + + +def get_user_id(): + uid = (os.getenv("JIANGCHANG_USER_ID") or os.getenv("OPENCLAW_USER_ID") or "").strip() + return uid or "_anon" + + +def get_skill_data_dir(): + path = os.path.join(get_data_root(), get_user_id(), SKILL_SLUG) + os.makedirs(path, exist_ok=True) + return path + + +def get_db_path(): + return os.path.join(get_skill_data_dir(), "account-manager.db") + + +def get_default_profile_dir(account_id): + path = os.path.join(get_skill_data_dir(), "profiles", account_id) + os.makedirs(path, exist_ok=True) + return path + + +def get_conn(): + return sqlite3.connect(get_db_path()) + + +def init_db(): + conn = get_conn() + try: + cur = conn.cursor() + cur.execute( + """ + CREATE TABLE IF NOT EXISTS accounts ( + id TEXT PRIMARY KEY, + name TEXT NOT NULL, + platform TEXT NOT NULL, + phone TEXT, + profile_dir TEXT, + extra_json TEXT, + created_at TEXT NOT NULL, + updated_at TEXT NOT NULL + ) + """ + ) + conn.commit() + finally: + conn.close() + + +def migrate_legacy_json_if_needed(): + init_db() + conn = get_conn() + try: + cur = conn.cursor() + cur.execute("SELECT COUNT(1) FROM accounts") + count = cur.fetchone()[0] + if count > 0: + return + finally: + conn.close() + + if not os.path.exists(LEGACY_ACCOUNTS_FILE): + return + + try: + with open(LEGACY_ACCOUNTS_FILE, "r", encoding="utf-8") as f: + raw = json.load(f) + except Exception: + return + + now = datetime.now().isoformat(timespec="seconds") + rows = [] + for platform, accounts in (raw or {}).items(): + for acc in accounts: + acc_id = str(acc.get("id") or "").strip() + if not acc_id: + continue + profile_dir = (acc.get("profile_dir") or "").strip() or get_default_profile_dir(acc_id) + base = { + "id": acc_id, + "name": str(acc.get("name") or acc_id), + "platform": str(acc.get("platform") or platform), + "phone": str(acc.get("phone") or ""), + "profile_dir": profile_dir, + } + extra = {k: v for k, v in acc.items() if k not in base} + rows.append( + ( + base["id"], + base["name"], + base["platform"], + base["phone"], + base["profile_dir"], + json.dumps(extra, ensure_ascii=False) if extra else None, + now, + now, + ) + ) + + if not rows: + return + + conn = get_conn() + try: + cur = conn.cursor() + cur.executemany( + """ + INSERT OR REPLACE INTO accounts + (id, name, platform, phone, profile_dir, extra_json, created_at, updated_at) + VALUES (?, ?, ?, ?, ?, ?, ?, ?) + """, + rows, + ) + conn.commit() + finally: + conn.close() + +def get_account_by_id(account_id): + migrate_legacy_json_if_needed() + conn = get_conn() + try: + cur = conn.cursor() + cur.execute( + "SELECT id, name, platform, phone, profile_dir, extra_json FROM accounts WHERE id = ?", + (account_id,), + ) + row = cur.fetchone() + if not row: + return None + acc = { + "id": row[0], + "name": row[1], + "platform": row[2], + "phone": row[3] or "", + "profile_dir": row[4] or get_default_profile_dir(row[0]), + } + if row[5]: + try: + extra = json.loads(row[5]) + if isinstance(extra, dict): + acc.update(extra) + except Exception: + pass + return acc + finally: + conn.close() def cmd_list(platform="all"): """ 修改后的 list 功能,能直观打出带着【手机号】的详细台账供AI查询匹配! 支持查全部(all),或指定(sohu) """ - accounts = load_accounts() - if platform == "all": - platforms_to_check = accounts.keys() - else: - platforms_to_check = [platform] - + migrate_legacy_json_if_needed() + conn = get_conn() found = False - for plat in platforms_to_check: - platform_accounts = accounts.get(plat, []) - for acc in platform_accounts: - phone = acc.get('phone', '未绑定手机') - print(f"账号ID:{acc['id']} | 名称:{acc['name']} | 手机号:{phone} | 平台:{acc['platform']}") - found = True - + try: + cur = conn.cursor() + if platform == "all": + cur.execute("SELECT id, name, phone, platform FROM accounts ORDER BY platform, id") + else: + cur.execute( + "SELECT id, name, phone, platform FROM accounts WHERE platform = ? ORDER BY id", + (platform,), + ) + rows = cur.fetchall() + finally: + conn.close() + + found = False + for row in rows: + phone = row[2] or "未绑定手机" + print(f"账号ID:{row[0]} | 名称:{row[1]} | 手机号:{phone} | 平台:{row[3]}") + found = True + if not found: print(f"ERROR:NO_ACCOUNTS_FOUND") def cmd_get(account_id): - accounts = load_accounts() - for platform_accounts in accounts.values(): - for acc in platform_accounts: - if acc["id"] == account_id: - print(json.dumps(acc, ensure_ascii=False)) - return + acc = get_account_by_id(account_id) + if acc: + print(json.dumps(acc, ensure_ascii=False)) + return print("ERROR:ACCOUNT_NOT_FOUND") def cmd_login(account_id): """首次登录:打开浏览器,用户手动登录,登录态自动保存到Profile目录""" - accounts = load_accounts() - target = None - for platform_accounts in accounts.values(): - for acc in platform_accounts: - if acc["id"] == account_id: - target = acc - break + target = get_account_by_id(account_id) if not target: print("ERROR:ACCOUNT_NOT_FOUND") From 9fcf0aaf7871ff86ad955d6c297f755e6f72c335 Mon Sep 17 00:00:00 2001 From: chendelian <116870791@qq.com> Date: Tue, 31 Mar 2026 14:18:51 +0800 Subject: [PATCH 23/27] chore: auto release commit (2026-03-31 14:18:51) --- accounts.json | 81 -------------------- scripts/account.py | 182 +++++++++++++++++++++++++-------------------- 2 files changed, 101 insertions(+), 162 deletions(-) delete mode 100644 accounts.json diff --git a/accounts.json b/accounts.json deleted file mode 100644 index dde789e..0000000 --- a/accounts.json +++ /dev/null @@ -1,81 +0,0 @@ -{ - "sohu": [ - { - "id": "sohu_account1", - "name": "搜狐账号1", - "phone": "13800000000", - "platform": "sohu", - "profile_dir": "D:/OpenClaw/chrome-profiles/sohu_account1" - }, - { - "id": "sohu_account2", - "name": "搜狐账号2", - "phone": "13900000000", - "platform": "sohu", - "profile_dir": "D:/OpenClaw/chrome-profiles/sohu_account2" - } - ], - "zhihu": [ - { - "id": "zhihu_account1", - "name": "知乎账号1", - "phone": "", - "platform": "zhihu", - "profile_dir": "D:/OpenClaw/chrome-profiles/zhihu_account1" - } - ], - "kimi": [ - { - "id": "kimi_account1", - "name": "Kimi账号1", - "phone": "13800000000", - "platform": "kimi", - "profile_dir": "D:/OpenClaw/chrome-profiles/kimi_account1" - } - ], - "deepseek": [ - { - "id": "deepseek_account1", - "name": "DeepSeek账号1", - "phone": "", - "platform": "deepseek", - "profile_dir": "D:/OpenClaw/chrome-profiles/deepseek_account1" - } - ], - "doubao": [ - { - "id": "doubao_account1", - "name": "豆包账号1", - "phone": "", - "platform": "doubao", - "profile_dir": "D:/OpenClaw/chrome-profiles/doubao_account1" - } - ], - "qianwen": [ - { - "id": "qianwen_account1", - "name": "千问账号1", - "phone": "", - "platform": "qianwen", - "profile_dir": "D:/OpenClaw/chrome-profiles/qianwen_account1" - } - ], - "yiyan": [ - { - "id": "yiyan_account1", - "name": "一言账号1", - "phone": "", - "platform": "yiyan", - "profile_dir": "D:/OpenClaw/chrome-profiles/yiyan_account1" - } - ], - "yuanbao": [ - { - "id": "yuanbao_account1", - "name": "元宝账号1", - "phone": "", - "platform": "yuanbao", - "profile_dir": "D:/OpenClaw/chrome-profiles/yuanbao_account1" - } - ] -} \ No newline at end of file diff --git a/scripts/account.py b/scripts/account.py index 4b68772..e5c91e1 100644 --- a/scripts/account.py +++ b/scripts/account.py @@ -2,7 +2,6 @@ import sys import json import os import sqlite3 -import subprocess from datetime import datetime # Windows GBK 编码兼容修复 @@ -12,7 +11,6 @@ if sys.platform == "win32": sys.stderr = io.TextIOWrapper(sys.stderr.buffer, encoding='utf-8', errors='replace') BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) SKILL_SLUG = "account-manager" -LEGACY_ACCOUNTS_FILE = os.path.join(BASE_DIR, "accounts.json") PLATFORM_URLS = { # 自媒体/图文平台 @@ -71,92 +69,35 @@ def init_db(): platform TEXT NOT NULL, phone TEXT, profile_dir TEXT, + login_status INTEGER DEFAULT 0, + last_login_at TEXT, extra_json TEXT, created_at TEXT NOT NULL, updated_at TEXT NOT NULL ) """ ) + _ensure_column(cur, "accounts", "login_status", "INTEGER DEFAULT 0") + _ensure_column(cur, "accounts", "last_login_at", "TEXT") conn.commit() finally: conn.close() -def migrate_legacy_json_if_needed(): +def _ensure_column(cur, table_name, col_name, ddl): + cur.execute(f"PRAGMA table_info({table_name})") + cols = [row[1] for row in cur.fetchall()] + if col_name not in cols: + cur.execute(f"ALTER TABLE {table_name} ADD COLUMN {col_name} {ddl}") + + +def get_account_by_id(account_id): init_db() conn = get_conn() - try: - cur = conn.cursor() - cur.execute("SELECT COUNT(1) FROM accounts") - count = cur.fetchone()[0] - if count > 0: - return - finally: - conn.close() - - if not os.path.exists(LEGACY_ACCOUNTS_FILE): - return - - try: - with open(LEGACY_ACCOUNTS_FILE, "r", encoding="utf-8") as f: - raw = json.load(f) - except Exception: - return - - now = datetime.now().isoformat(timespec="seconds") - rows = [] - for platform, accounts in (raw or {}).items(): - for acc in accounts: - acc_id = str(acc.get("id") or "").strip() - if not acc_id: - continue - profile_dir = (acc.get("profile_dir") or "").strip() or get_default_profile_dir(acc_id) - base = { - "id": acc_id, - "name": str(acc.get("name") or acc_id), - "platform": str(acc.get("platform") or platform), - "phone": str(acc.get("phone") or ""), - "profile_dir": profile_dir, - } - extra = {k: v for k, v in acc.items() if k not in base} - rows.append( - ( - base["id"], - base["name"], - base["platform"], - base["phone"], - base["profile_dir"], - json.dumps(extra, ensure_ascii=False) if extra else None, - now, - now, - ) - ) - - if not rows: - return - - conn = get_conn() - try: - cur = conn.cursor() - cur.executemany( - """ - INSERT OR REPLACE INTO accounts - (id, name, platform, phone, profile_dir, extra_json, created_at, updated_at) - VALUES (?, ?, ?, ?, ?, ?, ?, ?) - """, - rows, - ) - conn.commit() - finally: - conn.close() - -def get_account_by_id(account_id): - migrate_legacy_json_if_needed() - conn = get_conn() try: cur = conn.cursor() cur.execute( - "SELECT id, name, platform, phone, profile_dir, extra_json FROM accounts WHERE id = ?", + "SELECT id, name, platform, phone, profile_dir, login_status, last_login_at, extra_json FROM accounts WHERE id = ?", (account_id,), ) row = cur.fetchone() @@ -168,10 +109,12 @@ def get_account_by_id(account_id): "platform": row[2], "phone": row[3] or "", "profile_dir": row[4] or get_default_profile_dir(row[0]), + "login_status": int(row[5] or 0), + "last_login_at": row[6], } - if row[5]: + if row[7]: try: - extra = json.loads(row[5]) + extra = json.loads(row[7]) if isinstance(extra, dict): acc.update(extra) except Exception: @@ -185,16 +128,18 @@ def cmd_list(platform="all"): 修改后的 list 功能,能直观打出带着【手机号】的详细台账供AI查询匹配! 支持查全部(all),或指定(sohu) """ - migrate_legacy_json_if_needed() + init_db() conn = get_conn() found = False try: cur = conn.cursor() if platform == "all": - cur.execute("SELECT id, name, phone, platform FROM accounts ORDER BY platform, id") + cur.execute( + "SELECT id, name, phone, platform, login_status FROM accounts ORDER BY platform, id" + ) else: cur.execute( - "SELECT id, name, phone, platform FROM accounts WHERE platform = ? ORDER BY id", + "SELECT id, name, phone, platform, login_status FROM accounts WHERE platform = ? ORDER BY id", (platform,), ) rows = cur.fetchall() @@ -204,7 +149,8 @@ def cmd_list(platform="all"): found = False for row in rows: phone = row[2] or "未绑定手机" - print(f"账号ID:{row[0]} | 名称:{row[1]} | 手机号:{phone} | 平台:{row[3]}") + login_text = "已登录" if int(row[4] or 0) == 1 else "未登录" + print(f"账号ID:{row[0]} | 名称:{row[1]} | 手机号:{phone} | 平台:{row[3]} | 状态:{login_text}") found = True if not found: @@ -217,6 +163,48 @@ def cmd_get(account_id): return print("ERROR:ACCOUNT_NOT_FOUND") + +def cmd_add(account_id, name, platform, phone="", profile_dir=""): + init_db() + platform = (platform or "").strip().lower() + if platform not in PLATFORM_URLS: + print(f"ERROR:INVALID_PLATFORM (支持: {', '.join(sorted(PLATFORM_URLS.keys()))})") + return + + account_id = (account_id or "").strip() + name = (name or "").strip() or account_id + if not account_id: + print("ERROR:INVALID_ACCOUNT_ID") + return + + profile_dir = (profile_dir or "").strip() or get_default_profile_dir(account_id) + os.makedirs(profile_dir, exist_ok=True) + now = datetime.now().isoformat(timespec="seconds") + + conn = get_conn() + try: + cur = conn.cursor() + cur.execute( + """ + INSERT INTO accounts (id, name, platform, phone, profile_dir, login_status, last_login_at, extra_json, created_at, updated_at) + VALUES (?, ?, ?, ?, ?, 0, NULL, NULL, ?, ?) + ON CONFLICT(id) DO UPDATE SET + name = excluded.name, + platform = excluded.platform, + phone = excluded.phone, + profile_dir = excluded.profile_dir, + updated_at = excluded.updated_at + """, + (account_id, name, platform, phone, profile_dir, now, now), + ) + conn.commit() + finally: + conn.close() + + print(f"✅ 已保存账号:{account_id}({platform})") + print("ℹ️ 请继续执行登录:python account.py login <账号ID>") + + def cmd_login(account_id): """首次登录:打开浏览器,用户手动登录,登录态自动保存到Profile目录""" target = get_account_by_id(account_id) @@ -260,16 +248,48 @@ asyncio.run(main()) f.write(login_script) tmp_path = f.name - os.system(f'python "{tmp_path}"') + exit_code = os.system(f'python "{tmp_path}"') os.unlink(tmp_path) + if exit_code == 0: + _mark_login_success(account_id) + print("✅ 登录状态已标记为已登录") + else: + print("⚠️ 浏览器登录流程异常退出,登录状态未更新") + + +def _mark_login_success(account_id): + now = datetime.now().isoformat(timespec="seconds") + conn = get_conn() + try: + cur = conn.cursor() + cur.execute( + "UPDATE accounts SET login_status = 1, last_login_at = ?, updated_at = ? WHERE id = ?", + (now, now, account_id), + ) + conn.commit() + finally: + conn.close() if __name__ == "__main__": if len(sys.argv) < 2: - print("用法:python account.py list [platform] | get | login ") + print("用法:python account.py add [phone] [profile_dir] | list [platform] | get | login ") sys.exit(1) cmd = sys.argv[1] - if cmd == "list": + # 兼容写反顺序:python account.py sohu list + if len(sys.argv) >= 3 and sys.argv[2] == "list" and cmd in PLATFORM_URLS: + cmd_list(cmd) + elif cmd in PLATFORM_URLS and len(sys.argv) == 2: + cmd_list(cmd) + elif cmd == "add" and len(sys.argv) >= 5: + cmd_add( + sys.argv[2], + sys.argv[3], + sys.argv[4], + sys.argv[5] if len(sys.argv) >= 6 else "", + sys.argv[6] if len(sys.argv) >= 7 else "", + ) + elif cmd == "list": # 默认不传 platform 时打出 all cmd_list(sys.argv[2] if len(sys.argv) >= 3 else "all") elif cmd == "get" and len(sys.argv) >= 3: From 469b9c8bc708bb296c9eb0d4d4def8f498b6b2ca Mon Sep 17 00:00:00 2001 From: chendelian <116870791@qq.com> Date: Tue, 31 Mar 2026 15:24:22 +0800 Subject: [PATCH 24/27] chore: auto release commit (2026-03-31 15:24:21) --- scripts/account.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/scripts/account.py b/scripts/account.py index e5c91e1..d25318f 100644 --- a/scripts/account.py +++ b/scripts/account.py @@ -29,11 +29,16 @@ PLATFORM_URLS = { def get_data_root(): - return (os.getenv("JIANGCHANG_DATA_ROOT") or os.path.join(os.path.expanduser("~"), ".jiangchang-data")).strip() + env = (os.getenv("JIANGCHANG_DATA_ROOT") or "").strip() + if env: + return env + if sys.platform == "win32": + return r"D:\jiangchang-data" + return os.path.join(os.path.expanduser("~"), ".jiangchang-data") def get_user_id(): - uid = (os.getenv("JIANGCHANG_USER_ID") or os.getenv("OPENCLAW_USER_ID") or "").strip() + uid = (os.getenv("JIANGCHANG_USER_ID") or "").strip() return uid or "_anon" From ae1da709ef20d34b3cdafc1cf6737b95edd7e955 Mon Sep 17 00:00:00 2001 From: chendelian <116870791@qq.com> Date: Tue, 31 Mar 2026 17:00:42 +0800 Subject: [PATCH 25/27] chore: auto release commit (2026-03-31 17:00:42) --- SKILL.md | 8 +- scripts/__pycache__/account.cpython-312.pyc | Bin 6363 -> 25619 bytes scripts/account.py | 479 +++++++++++++++----- 3 files changed, 366 insertions(+), 121 deletions(-) diff --git a/SKILL.md b/SKILL.md index 9e5ad1b..a3225d7 100644 --- a/SKILL.md +++ b/SKILL.md @@ -1,7 +1,7 @@ --- name: 账号管理 description: 多平台多账号管理。管理各平台账号与Chrome Profile的对应关系,供publisher类Skill调用获取账号信息。 -version: 1.0.1 +version: 1.0.2 author: 深圳匠厂科技有限公司 metadata: openclaw: @@ -23,6 +23,12 @@ allowed-tools: ## 执行步骤 +### 添加账号(仅需平台,可选手机号;ID 数据库自增,名称自动生成如「搜狐1号」) +```bash +python3 {baseDir}/scripts/account.py add sohu +python3 {baseDir}/scripts/account.py add sohu 189xxxxxxxx +``` + ### 列出某平台所有账号 ```bash python3 {baseDir}/scripts/account.py list \ No newline at end of file diff --git a/scripts/__pycache__/account.cpython-312.pyc b/scripts/__pycache__/account.cpython-312.pyc index 741a4cd9f572356855e9a20c90dee817f8ee000e..dbd65246c40184270cea8e8902d87ec20b6b2fa1 100644 GIT binary patch literal 25619 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 diff --git a/scripts/account.py b/scripts/account.py index d25318f..0e36a16 100644 --- a/scripts/account.py +++ b/scripts/account.py @@ -2,29 +2,42 @@ import sys import json import os import sqlite3 +import subprocess +import tempfile +import time from datetime import datetime # Windows GBK 编码兼容修复 if sys.platform == "win32": import io - sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8', errors='replace') - sys.stderr = io.TextIOWrapper(sys.stderr.buffer, encoding='utf-8', errors='replace') + sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding="utf-8", errors="replace") + sys.stderr = io.TextIOWrapper(sys.stderr.buffer, encoding="utf-8", errors="replace") BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) SKILL_SLUG = "account-manager" PLATFORM_URLS = { - # 自媒体/图文平台 "sohu": "https://mp.sohu.com", "zhihu": "https://www.zhihu.com", "wechat": "https://mp.weixin.qq.com", - - # 大模型平台 "kimi": "https://kimi.moonshot.cn", "deepseek": "https://chat.deepseek.com", "doubao": "https://www.doubao.com", "qianwen": "https://tongyi.aliyun.com", "yiyan": "https://yiyan.baidu.com", - "yuanbao": "https://yuanbao.tencent.com" + "yuanbao": "https://yuanbao.tencent.com", +} + +# 新账号默认名称:搜狐1号 / sohu_2 等同平台序号 +_PLATFORM_NAME_CN = { + "sohu": "搜狐", + "zhihu": "知乎", + "wechat": "微信", + "kimi": "Kimi", + "deepseek": "DeepSeek", + "doubao": "豆包", + "qianwen": "通义", + "yiyan": "文心", + "yuanbao": "元宝", } @@ -53,7 +66,7 @@ def get_db_path(): def get_default_profile_dir(account_id): - path = os.path.join(get_skill_data_dir(), "profiles", account_id) + path = os.path.join(get_skill_data_dir(), "profiles", str(account_id)) os.makedirs(path, exist_ok=True) return path @@ -62,33 +75,6 @@ def get_conn(): return sqlite3.connect(get_db_path()) -def init_db(): - conn = get_conn() - try: - cur = conn.cursor() - cur.execute( - """ - CREATE TABLE IF NOT EXISTS accounts ( - id TEXT PRIMARY KEY, - name TEXT NOT NULL, - platform TEXT NOT NULL, - phone TEXT, - profile_dir TEXT, - login_status INTEGER DEFAULT 0, - last_login_at TEXT, - extra_json TEXT, - created_at TEXT NOT NULL, - updated_at TEXT NOT NULL - ) - """ - ) - _ensure_column(cur, "accounts", "login_status", "INTEGER DEFAULT 0") - _ensure_column(cur, "accounts", "last_login_at", "TEXT") - conn.commit() - finally: - conn.close() - - def _ensure_column(cur, table_name, col_name, ddl): cur.execute(f"PRAGMA table_info({table_name})") cols = [row[1] for row in cur.fetchall()] @@ -96,14 +82,123 @@ def _ensure_column(cur, table_name, col_name, ddl): cur.execute(f"ALTER TABLE {table_name} ADD COLUMN {col_name} {ddl}") +def _has_text_primary_id(cur): + cur.execute("PRAGMA table_info(accounts)") + for row in cur.fetchall(): + if row[1] == "id": + t = (row[2] or "").upper() + return "TEXT" in t or "CHAR" in t + return False + + +def _migrate_text_id_to_integer(conn): + cur = conn.cursor() + cur.execute( + """ + CREATE TABLE accounts_new ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + name TEXT NOT NULL, + platform TEXT NOT NULL, + phone TEXT, + profile_dir TEXT, + url TEXT, + login_status INTEGER DEFAULT 0, + last_login_at TEXT, + extra_json TEXT, + created_at TEXT NOT NULL, + updated_at TEXT NOT NULL + ) + """ + ) + cur.execute( + "SELECT name, platform, phone, profile_dir, login_status, last_login_at, extra_json, created_at, updated_at FROM accounts ORDER BY rowid" + ) + rows = cur.fetchall() + for row in rows: + name, platform, phone, profile_dir, login_status, last_login_at, extra_json, created_at, updated_at = row + plat = (platform or "").strip().lower() + url = PLATFORM_URLS.get(plat, "") + cur.execute( + """ + INSERT INTO accounts_new + (name, platform, phone, profile_dir, url, login_status, last_login_at, extra_json, created_at, updated_at) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?) + """, + ( + name, + plat, + phone or "", + profile_dir or "", + url, + int(login_status or 0), + last_login_at, + extra_json, + created_at, + updated_at, + ), + ) + cur.execute("DROP TABLE accounts") + cur.execute("ALTER TABLE accounts_new RENAME TO accounts") + + +def init_db(): + conn = get_conn() + try: + cur = conn.cursor() + cur.execute("SELECT name FROM sqlite_master WHERE type='table' AND name='accounts'") + if not cur.fetchone(): + cur.execute( + """ + CREATE TABLE accounts ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + name TEXT NOT NULL, + platform TEXT NOT NULL, + phone TEXT, + profile_dir TEXT, + url TEXT, + login_status INTEGER DEFAULT 0, + last_login_at TEXT, + extra_json TEXT, + created_at TEXT NOT NULL, + updated_at TEXT NOT NULL + ) + """ + ) + else: + if _has_text_primary_id(cur): + _migrate_text_id_to_integer(conn) + cur = conn.cursor() + _ensure_column(cur, "accounts", "url", "TEXT") + cur.execute( + "SELECT id, platform FROM accounts WHERE url IS NULL OR TRIM(COALESCE(url,'')) = ''" + ) + for rid, plat in cur.fetchall(): + pu = PLATFORM_URLS.get((plat or "").strip().lower(), "") + if pu: + cur.execute("UPDATE accounts SET url = ? WHERE id = ?", (pu, rid)) + conn.commit() + finally: + conn.close() + + +def _normalize_account_id(account_id): + if account_id is None: + return None + s = str(account_id).strip() + if s.isdigit(): + return int(s) + return s + + def get_account_by_id(account_id): init_db() + aid = _normalize_account_id(account_id) conn = get_conn() try: cur = conn.cursor() cur.execute( - "SELECT id, name, platform, phone, profile_dir, login_status, last_login_at, extra_json FROM accounts WHERE id = ?", - (account_id,), + "SELECT id, name, platform, phone, profile_dir, url, login_status, last_login_at, extra_json FROM accounts WHERE id = ?", + (aid,), ) row = cur.fetchone() if not row: @@ -114,12 +209,13 @@ def get_account_by_id(account_id): "platform": row[2], "phone": row[3] or "", "profile_dir": row[4] or get_default_profile_dir(row[0]), - "login_status": int(row[5] or 0), - "last_login_at": row[6], + "url": row[5] or PLATFORM_URLS.get(row[2], ""), + "login_status": int(row[6] or 0), + "last_login_at": row[7], } - if row[7]: + if row[8]: try: - extra = json.loads(row[7]) + extra = json.loads(row[8]) if isinstance(extra, dict): acc.update(extra) except Exception: @@ -128,14 +224,17 @@ def get_account_by_id(account_id): finally: conn.close() + +def _default_name_for_platform(platform: str, index: int) -> str: + cn = _PLATFORM_NAME_CN.get(platform) + if cn: + return f"{cn}{index}号" + return f"{platform}_{index}" + + def cmd_list(platform="all"): - """ - 修改后的 list 功能,能直观打出带着【手机号】的详细台账供AI查询匹配! - 支持查全部(all),或指定(sohu) - """ init_db() conn = get_conn() - found = False try: cur = conn.cursor() if platform == "all": @@ -159,7 +258,8 @@ def cmd_list(platform="all"): found = True if not found: - print(f"ERROR:NO_ACCOUNTS_FOUND") + print("ERROR:NO_ACCOUNTS_FOUND") + def cmd_get(account_id): acc = get_account_by_id(account_id) @@ -169,133 +269,272 @@ def cmd_get(account_id): print("ERROR:ACCOUNT_NOT_FOUND") -def cmd_add(account_id, name, platform, phone="", profile_dir=""): +def cmd_add(platform: str, phone: str = ""): + """添加账号:仅平台 + 可选手机号;ID 自增,名称按平台序号自动生成。""" init_db() platform = (platform or "").strip().lower() if platform not in PLATFORM_URLS: print(f"ERROR:INVALID_PLATFORM (支持: {', '.join(sorted(PLATFORM_URLS.keys()))})") return - account_id = (account_id or "").strip() - name = (name or "").strip() or account_id - if not account_id: - print("ERROR:INVALID_ACCOUNT_ID") - return - - profile_dir = (profile_dir or "").strip() or get_default_profile_dir(account_id) - os.makedirs(profile_dir, exist_ok=True) + phone = (phone or "").strip() + url = PLATFORM_URLS[platform] now = datetime.now().isoformat(timespec="seconds") conn = get_conn() try: cur = conn.cursor() + cur.execute("SELECT COUNT(*) FROM accounts WHERE platform = ?", (platform,)) + next_idx = cur.fetchone()[0] + 1 + name = _default_name_for_platform(platform, next_idx) cur.execute( """ - INSERT INTO accounts (id, name, platform, phone, profile_dir, login_status, last_login_at, extra_json, created_at, updated_at) - VALUES (?, ?, ?, ?, ?, 0, NULL, NULL, ?, ?) - ON CONFLICT(id) DO UPDATE SET - name = excluded.name, - platform = excluded.platform, - phone = excluded.phone, - profile_dir = excluded.profile_dir, - updated_at = excluded.updated_at + INSERT INTO accounts (name, platform, phone, profile_dir, url, login_status, last_login_at, extra_json, created_at, updated_at) + VALUES (?, ?, ?, '', ?, 0, NULL, NULL, ?, ?) """, - (account_id, name, platform, phone, profile_dir, now, now), + (name, platform, phone, url, now, now), + ) + new_id = cur.lastrowid + profile_dir = get_default_profile_dir(new_id) + cur.execute( + "UPDATE accounts SET profile_dir = ?, updated_at = ? WHERE id = ?", + (profile_dir, now, new_id), ) conn.commit() finally: conn.close() - print(f"✅ 已保存账号:{account_id}({platform})") - print("ℹ️ 请继续执行登录:python account.py login <账号ID>") + print(f"✅ 已保存账号:ID {new_id} | {name} | {platform}") + print(f"ℹ️ 登录请执行:python account.py login {new_id}") + + +def _win_find_exe(candidates): + for p in candidates: + if p and os.path.isfile(p): + return p + return None + + +def resolve_chromium_channel(): + """ + Playwright channel: 'chrome' | 'msedge' | None + Windows 优先 Chrome,其次 Edge;其它系统默认 chrome(由 Playwright 解析 PATH)。 + """ + if sys.platform != "win32": + return "chrome" + + pf = os.environ.get("ProgramFiles", r"C:\Program Files") + pfx86 = os.environ.get("ProgramFiles(x86)", r"C:\Program Files (x86)") + local = os.environ.get("LocalAppData", "") + + chrome = _win_find_exe( + [ + os.path.join(pf, "Google", "Chrome", "Application", "chrome.exe"), + os.path.join(pfx86, "Google", "Chrome", "Application", "chrome.exe"), + os.path.join(local, "Google", "Chrome", "Application", "chrome.exe") if local else "", + ] + ) + if chrome: + return "chrome" + + edge = _win_find_exe( + [ + os.path.join(pfx86, "Microsoft", "Edge", "Application", "msedge.exe"), + os.path.join(pf, "Microsoft", "Edge", "Application", "msedge.exe"), + os.path.join(local, "Microsoft", "Edge", "Application", "msedge.exe") if local else "", + ] + ) + if edge: + return "msedge" + + return None + + +def _print_browser_install_hint(): + print("❌ 未检测到 Google Chrome 或 Microsoft Edge,请先安装后再登录:") + print(" • Chrome: https://www.google.com/chrome/") + print(" • Edge: https://www.microsoft.com/zh-cn/edge") + + +def _login_timeout_seconds(): + try: + return max(60, int((os.getenv("JIANGCHANG_LOGIN_TIMEOUT_SECONDS") or "300").strip())) + except ValueError: + return 300 + + +def _verify_login(profile_dir: str, platform: str, start_url: str, channel: str) -> bool: + try: + from playwright.sync_api import sync_playwright + except ImportError: + print("⚠️ 未安装 playwright,跳过登录校验(请: pip install playwright && playwright install)") + return False + + try: + with sync_playwright() as p: + ctx = p.chromium.launch_persistent_context( + user_data_dir=profile_dir, + headless=True, + channel=channel, + args=["--disable-blink-features=AutomationControlled"], + ) + try: + page = ctx.pages[0] if ctx.pages else ctx.new_page() + page.goto(start_url, wait_until="domcontentloaded", timeout=45000) + time.sleep(2) + u = (page.url or "").lower() + if platform == "sohu": + if "passport" in u or "/login" in u: + return False + return True + if "login" in u or "signin" in u or "passport" in u: + return False + return True + finally: + ctx.close() + except Exception: + return False def cmd_login(account_id): - """首次登录:打开浏览器,用户手动登录,登录态自动保存到Profile目录""" target = get_account_by_id(account_id) - if not target: print("ERROR:ACCOUNT_NOT_FOUND") return + channel = resolve_chromium_channel() + if not channel: + _print_browser_install_hint() + return + + try: + import playwright # noqa: F401 + except ImportError: + print("ERROR:需要 playwright:pip install playwright && playwright install chromium") + return + profile_dir = target["profile_dir"] os.makedirs(profile_dir, exist_ok=True) - url = PLATFORM_URLS.get(target["platform"], "https://www.google.com") + url = (target.get("url") or "").strip() or PLATFORM_URLS.get(target["platform"], "https://www.google.com") + platform = (target.get("platform") or "").strip().lower() + timeout_sec = _login_timeout_seconds() - print(f"正在为账号 [{target['name']}] 打开浏览器...") - print(f"请在浏览器中完成登录,登录后直接关闭浏览器窗口即可。") - print(f"登录态将自动保存到:{profile_dir}") + browser_name = "Google Chrome" if channel == "chrome" else "Microsoft Edge" + print(f"正在为账号 [{target['name']}] 打开 {browser_name} …") + print(f"访问地址:{url}") + print("请在窗口内完成登录(扫码/输入密码)。到达等待时间后将自动关闭窗口,您也可提前手动关闭。") + print(f"最长等待:{timeout_sec} 秒") - # 用Playwright打开持久化浏览器,等待用户手动登录 - login_script = f""" -import asyncio -from playwright.async_api import async_playwright -async def main(): - async with async_playwright() as p: - browser = await p.chromium.launch_persistent_context( - user_data_dir=r"{profile_dir}", - headless=False, - channel="chrome", # 【修改处】也加上这行 - no_viewport=True, # 确保浏览器内容视口也最大化 - args=["--start-maximized"] - ) - page = browser.pages[0] if browser.pages else await browser.new_page() - await page.goto("{url}") - print("浏览器已打开,请完成登录后关闭窗口...") - # 移除自动超时,无限等待直到用户扫码完手动关闭 - await browser.wait_for_event("close", timeout=0) - print("登录态已保存!") -asyncio.run(main()) + # Playwright 同步 API 宜在独立子进程中跑浏览器,避免线程与事件循环问题 + runner = r"""import json, sys +from playwright.sync_api import sync_playwright +with open(sys.argv[1], encoding="utf-8") as f: + c = json.load(f) +with sync_playwright() as p: + ctx = p.chromium.launch_persistent_context( + user_data_dir=c["profile_dir"], + headless=False, + channel=c["channel"], + no_viewport=True, + args=["--start-maximized"], + ) + try: + page = ctx.pages[0] if ctx.pages else ctx.new_page() + page.goto(c["url"], wait_until="domcontentloaded", timeout=60000) + try: + ctx.wait_for_event("close", timeout=c["timeout_sec"] * 1000) + except Exception: + pass + try: + ctx.close() + except Exception: + pass + except Exception as e: + print(e, file=sys.stderr) + raise """ - import tempfile - with tempfile.NamedTemporaryFile(mode='w', suffix='.py', - delete=False, encoding='utf-8') as f: - f.write(login_script) - tmp_path = f.name + cfg = { + "channel": channel, + "profile_dir": profile_dir, + "url": url, + "timeout_sec": timeout_sec, + } + with tempfile.NamedTemporaryFile( + mode="w", suffix=".json", delete=False, encoding="utf-8" + ) as jf: + json.dump(cfg, jf, ensure_ascii=False) + cfg_path = jf.name + with tempfile.NamedTemporaryFile( + mode="w", suffix=".py", delete=False, encoding="utf-8" + ) as pf: + pf.write(runner) + py_path = pf.name + try: + r = subprocess.run( + [sys.executable, py_path, cfg_path], + timeout=timeout_sec + 180, + ) + if r.returncode != 0: + print("⚠️ 浏览器进程异常退出,仍将尝试检测登录状态") + except subprocess.TimeoutExpired: + print("⚠️ 等待浏览器超时,仍将尝试检测登录状态") + finally: + try: + os.unlink(cfg_path) + except OSError: + pass + try: + os.unlink(py_path) + except OSError: + pass - exit_code = os.system(f'python "{tmp_path}"') - os.unlink(tmp_path) - if exit_code == 0: - _mark_login_success(account_id) - print("✅ 登录状态已标记为已登录") + time.sleep(1.5) + print("正在检测登录状态…") + ok = _verify_login(profile_dir, platform, url, channel) + _mark_login_status(target["id"], ok) + if ok: + print("✅ 检测到已登录,状态已写入数据库") else: - print("⚠️ 浏览器登录流程异常退出,登录状态未更新") + print("⚠️ 未检测到有效登录,状态为未登录。请关闭其他占用该用户目录的浏览器后重试,或延长 JIANGCHANG_LOGIN_TIMEOUT_SECONDS 后再登录。") -def _mark_login_success(account_id): +def _mark_login_status(account_id, success: bool): now = datetime.now().isoformat(timespec="seconds") conn = get_conn() try: cur = conn.cursor() - cur.execute( - "UPDATE accounts SET login_status = 1, last_login_at = ?, updated_at = ? WHERE id = ?", - (now, now, account_id), - ) + if success: + cur.execute( + "UPDATE accounts SET login_status = 1, last_login_at = ?, updated_at = ? WHERE id = ?", + (now, now, account_id), + ) + else: + cur.execute( + "UPDATE accounts SET login_status = 0, updated_at = ? WHERE id = ?", + (now, account_id), + ) conn.commit() finally: conn.close() + if __name__ == "__main__": if len(sys.argv) < 2: - print("用法:python account.py add [phone] [profile_dir] | list [platform] | get | login ") + print( + "用法:python account.py add [phone] | list [platform] | get | login " + ) sys.exit(1) cmd = sys.argv[1] - # 兼容写反顺序:python account.py sohu list if len(sys.argv) >= 3 and sys.argv[2] == "list" and cmd in PLATFORM_URLS: cmd_list(cmd) elif cmd in PLATFORM_URLS and len(sys.argv) == 2: cmd_list(cmd) - elif cmd == "add" and len(sys.argv) >= 5: - cmd_add( - sys.argv[2], - sys.argv[3], - sys.argv[4], - sys.argv[5] if len(sys.argv) >= 6 else "", - sys.argv[6] if len(sys.argv) >= 7 else "", - ) + elif cmd == "add" and len(sys.argv) >= 3: + plat = sys.argv[2].lower() + phone_arg = sys.argv[3] if len(sys.argv) >= 4 else "" + cmd_add(plat, phone_arg) elif cmd == "list": - # 默认不传 platform 时打出 all cmd_list(sys.argv[2] if len(sys.argv) >= 3 else "all") elif cmd == "get" and len(sys.argv) >= 3: cmd_get(sys.argv[2]) @@ -303,4 +542,4 @@ if __name__ == "__main__": cmd_login(sys.argv[2]) else: print("参数错误") - sys.exit(1) \ No newline at end of file + sys.exit(1) From 58dc81449d87d2938d8d4d4d242d03778e2d2254 Mon Sep 17 00:00:00 2001 From: chendelian <116870791@qq.com> Date: Tue, 31 Mar 2026 18:19:37 +0800 Subject: [PATCH 26/27] chore: auto release commit (2026-03-31 18:19:36) --- scripts/__pycache__/account.cpython-312.pyc | Bin 25619 -> 25173 bytes scripts/account.py | 240 ++++++++++---------- 2 files changed, 120 insertions(+), 120 deletions(-) diff --git a/scripts/__pycache__/account.cpython-312.pyc b/scripts/__pycache__/account.cpython-312.pyc index dbd65246c40184270cea8e8902d87ec20b6b2fa1..4aef0a751a512ab5f334c1bbe02a6901ed7231cc 100644 GIT binary patch delta 10277 zcmb6<2~<>9mjBnUeW6&Y*uf8iQtU{?l_&}-f}n!N5KUR~KUi2)kzc=5?4paLHIb+R z^GsZmHr;lj(I|;oj7GDWWnxa+g!GB2HZ$po#nLmUCCi-2$xP3gd;hNjw9RzS)4KmH z_ucp2efQmW?|qOD=|5biqpp}tdJ58%r>{7lUA#9ckzTuZYIPY$ag0mbu4~fK6i>BT z5argyDL3U%l&~H!X_ukNprCjIVQCA|(+PyHv81Uu^(oCsM()K^c2lH(1XBrw)5)Jq zafqIBJGwc7&MOEz&7x?VO!{am$(R;5AsgS89YGua@^R!7Iw@e`v`tn{*A&m``Ggh) zXV|W6O5}{Zj!#GeWT@5gNtkO$gA=>YS& zGJpl#41k3a_DrCv=Zk>iETCA-Rm_G89BK~y=7t;fTsbtBaCbwuY1}+OS8(&8rj);n ztHAy2qlFh>f2PCMxX5}{ieteh1?+M%)Xm@)o}^_i(-f4cwvKV0M{-={I1bW?)Q^9; zO5}Odtcjzns@k*`Mw#?8Jr+p&wMddT6F-Yj*-QyK=s%U8H9lG=9d1dVNI7qT)a#De z++wM1!$kaSc_z#LubH&@$XohkebbzY9UGQNOECXT$0}Tco92p}=0YzUKZl=t5*Uk# zrf}+`fzr~yr4$&a zH0>Xy1f$O2YQ|>?<-aRRopdK{s~bUf)!FEvmUD=_=xpbWSg7w1ogTNtMVi!e13uI^ z`l4JmbnH;*^|PVg{^6IO4fVb@{LJy2ZydUD?y2Vh4{tGJNJ?oawp8LJ#j4x_FvhUR%+)sD9-# z`>K_-4J^#y#<}j{M_!gkVZEOGjXs(7Mvt492+Q<(&x~}3S3cOYYw*Z|m9Sl&R;SzU6&<3_%U0Jl2C5cUtsGlCTUoWJVpVM;TP#g`@W7c+ z-?O3DUXr&2_Pm&d3burW7U2ERz88nL9|9p5Jo?PY^SeWRz@~#Rcw}lWhgY=ABRWJJ z4F{7cAL$-`b1yJ&?%=6s!W+J`FZAfI2lu}^vLE@m%I)0B4n6lcyQ*>F%{LAY9(i6y z8-DYV8|ThTTcsy^<1G>c2YXJyriWhGKY02Tc4owU|wfd|p%?BipPEmWGs@acXK zw9wAOp$q5lIIhg!4m==tq=KSSDpMIe(hbWWJpA(Dll^z5Wt*c9ssŌu^Vo}v&L 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 delta 9939 zcma(%33OA}m4DN|Sk~fAvMukHZOjg4HNs2qf(^zH;t&*}FN3_u=1T_b%8|)Q3Jzp! z^OC`7l8Jjd%{W`*8QPxoOq!)3Z5ndsKuC(CGM(m3A>nkUaR`}qn4X!r_kGD0#B(|y zbpLzz-FM%1*Z2MLd6vA#8ZQ_OS_Yo~Iej5;qJF}d!8#^N8U#PnWg^Q{E!BK?DZ@w) zij2tiXgV|;Vagm@a0BnPL{j+eG^#Oh=mN0GKXj1I!R}0A`B0k3^Y{EYS%SyO;;i zA?5?j77GC8h=l-i#Ug-Cu^3>USmMtYOZ^3+%b(w=6w4kFIts;m#EM7Qjv{dhP!x;X zD9uu67sO?cC^|~S<$y00R{(U0D*={?s{odZtASU^MgVKzw-$abe?_NOtOBM>#C1SY zDXs?$CvE`DQvWh>BcAp+E4>0IvmDM;B%h{PisCyZZtCVh7P6pirMNkRC(8&iwkuso2molIx;pR(O0Dw0?5D-JS4NfT!U`&VX210(x6A&ekab`7{O6Oe2Nn zAK56AY`{o42yjTLl>EW$a3M08n;E%Gy^YNy`Dfg_K!~pz{vv`GaLpcPZs%EzAgc#Q0EHl9pgQ7t?xuzP^EA z7#Bt7h{EQKFFBp=puf{Q&=dCbOMRVz9=``?x(0}9;TKv-mRi%uge{LwIg`w2O=M}R zmCV^3WKIibk!CHxnMunKEKFUEoPB-4ptJ!R8c9Q%qYztt0)WJ$h5`@2uYL^REE8e6 zSTGS*ON4!z5xHmi1!0L0HZ!}jmKhU#+(E|2J)mNkVId-jJf7$TcUmJb!wSG~5ycT! zvd39jq5|cmh;YDB8R4I1o>eYz9b^W97Yn~(;V9!$wG5eew7Kuu?skT~2Vf@x!Op(Y z+pOybO39Gk-P~T+=4^LwZLV_;>7DM{T4zmb^X}~}&fBaLwp!&s=JySR{V{c?KkPf? z?dfqvl`-BoAjPyY!=SepT77*z9$Ml>u_wk$eMe&g;6foK4CADaeEvp{KNuR2{IHQd z1HD0M8??8;FVqQOgqamoCz_5oT{IU?bxbwSuoq1$J`z^W=Hxxs^Hk5%!C&c?jCY4l z){L{K>M!b+jMM^{Ri%uFFRARaYTcM_R5#u;wSKB%y63g3*UH{d$Li)e zo>4pg^Cr*eD?_7l3wFpQZ3cER zE+F1O*_No<3+;7#+han_Hg{V+DE}60a3h&wpugWQ0ctCv>Ji0;qaj=i?7Y11R?i`C z$P@M-L!}7xdZj_so*h848-Ae&04(gjX~{=IX*!^pzoZT z(Wg>Hx?{hMFecsa^Kc4@{VugDV1v7OP#T94c3v)8$o47?Y#woD=2Q-I5f0AcD4Yc! z;rD4!Z9q|!!>VERux400tQ*!x6!Ej*i8J3yrZ%g&E)7{;YUd4MEtyDhm?H*}Pk515u9tb%bTH5RG z0n1WbSMT24-0obK;1EUjPG^rd6!yqHyx~Qdft*TSPgkffxJaW<@`DQ%J@KB4tQ3bB z=toC;H&u3X^ZX7h?A*|aU3JZMHSOe6M-kCwmy*_s^g?HSTkCdbV&|Q$ZMCqVt@k;> zxPj`RN)G*+{JK2PgoiTE6h9sI@!-Z{ zd>|NhrJ#)lcZOyoCcv^nvhfg5h$IJM6;wiEx+F&$(1$*eo68{ul?`~-39cC~A z3%bZIW{wjKra3*6YMV7#XHB*BOQ7H3V_v!*Qh z|C~){Fn(eIUET+%O+DB}OIvbyj_ZkK096mkp`O={g7Gl?juG(G9rEYMgkM zs0K^Rb#jSDu$S#%FPW}P@?D2RfPx(h>OrOeu~;c$HRQ?kZI#|k7(Q9ys5~udFTqxl z!#k|xxT=VyCF^1al9DLY=`x8v*(zd_IZ68C<_OW+1v`jUXR=x>W&n}MdWW_oF-!f1 z#Y6I}|J&4Ib98YVmt^7+ns%q@L2OK&Ff7smI7F!#z)+iP2Au)1!ij3z*(dehWd`rv zR@YYN#HBavDyI$xETgl%tzmmwwe^6yxv<-2yPeT-8}ltskkK z;|fS`MsakkX%tL~G1dAtZ!#Ef6*8v6shW>cO0SyAr)xhlEuT%xI@>+bO?t=qtNN_T zl~da3x|uZ}>DSMK<$=DlCJ}6r!3d@ZLfL&!?R$DZL{)}+*$K@!Ki+=TmVd=oe92Zk zExf3m;jWadyHv97a><4(CGJZl?#m_j)x6bq*|u$*i&<>5*7UPY6HOPLjqkNx+}ZwK z!^hS=5M==`2(vT>41rG@eyi4xX-Bn>>;H68kPEj%W?0zinzY(1=AD9KfakN!wH4$- z#`y9oXmJVBDtOU7ji~}5F>o*tmiAx^C#{B8j17fA?HCLw$>~hHq7N#sk(VkXV0704TC>uz8oa03nN4M@Hq1d|%!CEbSr2a`Gw+>e0j2;AB*5{Ak*FwJwN zQmK(}BkmGmsS|OB7z@BPSX>a=J>+msRR-Nr-V35RFR9e`lDBg*^YfrbEETiOxQcoX z&}e-G@NdH}w2J&WC%ev}u+19HW8I_Or+Slo6Jx03W(}s3^|ur}cp5bWaeU8c&#B-^ z{*x5*tR;P}6y=19$6OKdklkL40`?K(alGz5a8|5!{Z`NIQJkW4xs%RzmG1ch$u zaVW<)UvMEoAzSjCiW7kT9r5RFVGGVZpZ5n=c@hvqZR8IHo5+=dFxy0$3o~>QAmfKh z`U>~6hsi$`F5QmWd-oBz5qAgyN-;z-62?4gc@7J^zLz$3!hM2BPeAG*JBqTcd5Bi~ zdPPr90InV$hwAUo9WT1VZW@82^caGp0AdPpptnC1R}$$VK+UTOHmi^Y9Hm5{re?{L zC2NWbph$&~%9jtdD)FEfr%k(c&Dg*c|K(T-v8edO8lDt7a^UzLa0s8!b2EFh+CL)J=qlz8GR&>Oy6#;BU3;KnrQ*yYTCD*Qnl1f|cVO_&leCbAj%4z|rpU`!pvtUMqdqlPD@ zglqzHb2K|4w4~HsCHpKCDX%C>&q1S!1*$NyRu0#^Dk-hryDM6^Dk@H zknzfK)nvisk&F6b;##`bxM6E%y^8s{%3Pnx`&e?=VkO^O>N8Lu5Wi8(CYEJ|nN$M| zo}-Y|b%H{B;3i1=TNrUI*|BU5=;(=M<2>A4W~}nVDLk}l_Z~FW@>=l{5U!4bM*>n` zQ1;c>lU~-*TSk`xym2S!WaqJ}HJGRPygkzM*h}z>2mQ%jnz&ZyD1Hb{o6j|`E@oli z(3%p~L{DuQGNsy-M_yc;r}#0{z2xHBVz}A+=LQ*wXp zI)~z?K!1>wuk&!y{p8I}OW5C#f8X>4*JVY`D%UMYVnnsrcb|ChtA`L4bTUs3akYV7 zF>eI9Rv%$Pf(SAVcl%iyP&zrjB^#?#Rl*&0bB<`DzuOWjDUdL4^#av^DK zFzE-GWBh&0QMP~yJIHvI9TJBAEg!OPk{b8g`>8jSZ4jEAn7ZE^3iX5d4=I<@I2I2D z@_EpSz`Z(gQsCgBaAjyP=<|5{1Jc{rS=Z2uUFAJ}jGS|Ci3+HHF-54y z@9&p|9^*9G97mw!(#UR=I)pc{=}iR4PWm1IxMB<+gVfP`&>v$5LO92HHIO4WgXf4} z3Um(2w+PY|pa`NabORVkXCOyY*#23)c21kFuzg~1TsGv)F;+6MwURZEx3(^eUNu?I zYA3Xl%bzgJ+A`1Xo!EP3|CITPwd|6$?6S3DoSn7UCy#t+DVj~so7y=Moi3g6zOZCA zw-6IQ>sif&<_X;&T~>_)*aFC59L_oIVl7U_YM=B@Y@Di^KK$KHGmUTM{eA1Ky}}8x z;39kqnGDBoO_n?PlY48N(G_v>Cv~?M#{I8bWsKf-lVM?zDb_h2-nT=?VJ{w5))q0N zsyZh#o>yn(|G`~dTh0A!g$c^z(~iYe%z!1{ymlmD)3EkI9TOTIA|)4;!QkTxnJDlz>T zVOUFAs&jMY?~(SG9f|yPH8ZSRaB%3Z*5mD*nDugY;&UuGBVAX1eDtK(upU17!oP$^ zgj5)Y$b`?D@X7PeAq)`%xHE`7-I^rBU|5u^@ougV@RlTo?J6Z~x@AMze7z!3T6jl| zQT;(4mYs|K`(t!pIh9xvgLutglHjR^>C###*vG?@zWc=p=Z_r7@j^!10QfA^E;u8qBVeQf;N z=;PN%PtDT+s{y<$yZ+3`jVIrf`F-*FcLoH8$zq(J{oNU_6Lcp)&8`z~Nh_S8!4P?* zc{%yc_S$ouEhkt~wKJ7$-&vUHyFVX|ao+%ZyYu_u!wwBE7Y}yZbAhpD}F?35}x}@GQ_UMqJ5(li8Fn4kZF`>6l^n;ri=XHZ&%vTzyY|z@%cj`_%t;T zlj&YTYHftPhdvWd!I&VrCC~9_fzXPRsY%km}KlO&Rt|7|hcx`WNgmzfW@H z^9kU{ncZb|GssERKS)1x##kxF^>v3*0Xgr+5(U|vX=ti}E?4>*sK(Hhr4eq=kI0*Q zs@F4&u9dsRFp3@ARY7yIb+T!4)3k6=i{EMHlvze$q<*dn{LE~6K76(T_@zdnQ;yW! z(x2OQZ#kRW%Q5=&n+&H&xvDl?GA{ zF@^uR)2(Q^6#(X znzEVqH?UaV$YZ%$)1>1sXjz0tO;Z+sA(KUz%{G_u7jkV)tAz`zXnD1!c`d8Du$hB~ z4;Z$YReiwnSXS7Y?ZO9HNc4eS(_EtZppeoOvsf+x8uI0RPOg?Ed8zsMnFl_-SxqAQ z@_gDb%!O5CNk=-|9YRn>9_&z)Gg%f+kZz3Z+dCrj|MEI^0OjG1L zNV!sERJ2G>!U;)#4PdApuC)rt_WK8-YML-eG`4>l+czL+L$C$GLPDURvLyWnV(&}^ zlH*=%{e1#_jpFbq)Zk`HyLg;mwC`}nj9b6e!2HZ$-kQtD)E#+Je1iwW<6$za>RR;Kc*%vdkLvqv5dap$O=7AhGoBZ^FvMBmDIrqXP#sA^}OZcUq z{!xH@X+n2!8{&2#@FAd?KD}`n!Rjgmdk~yJfN46-v}tZc(-mpnoJbhn$AkkZeSJMt z)2XIOzbDcCncFa*faK#CTq5$IigJGFMWlva2YW#Alk){=xvky^wr`-vzgg13`M`fB zR7pPF?<9if1gj+f=n3Qr>~{91QOTy>G%sbVZ;EUYYrDCFRkDVgW+!W#t0yZT+_7rT vn8Vs8LvswgrhWAFn*4gNhSn~&@BIoJZ*FCMEF0zKn)!1dKNw-@Uvm5}u#kfc diff --git a/scripts/account.py b/scripts/account.py index 0e36a16..a979e45 100644 --- a/scripts/account.py +++ b/scripts/account.py @@ -6,6 +6,7 @@ import subprocess import tempfile import time from datetime import datetime +from typing import Optional # Windows GBK 编码兼容修复 if sys.platform == "win32": @@ -15,6 +16,40 @@ if sys.platform == "win32": BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) SKILL_SLUG = "account-manager" +# SQLite 无独立 DATETIME 类型:时间统一存 INTEGER Unix 秒(UTC),查询/JSON 再转 ISO8601。 +# 下列 DDL 含注释,会原样写入 sqlite_master,便于 Navicat / DBeaver 等查看建表语句。 +ACCOUNTS_TABLE_SQL = """ +/* 多平台账号表:与本地 Chromium/Edge 用户数据目录(profile)绑定,供发布类技能读取登录态 */ +CREATE TABLE accounts ( + id INTEGER PRIMARY KEY AUTOINCREMENT, -- 账号主键(自增) + name TEXT NOT NULL, -- 展示名称,如「搜狐1号」 + platform TEXT NOT NULL, -- 平台标识,与内置 PLATFORM_URLS 键一致,如 sohu + phone TEXT, -- 可选绑定手机号 + profile_dir TEXT, -- Playwright 持久化用户数据目录(绝对路径) + url TEXT, -- 平台入口或登录页 URL + login_status INTEGER NOT NULL DEFAULT 0, -- 是否已登录:0 否 1 是(由脚本校验后写入) + last_login_at INTEGER, -- 最近一次登录成功时间,Unix 秒 UTC;未登录过为 NULL + extra_json TEXT, -- 扩展字段 JSON + created_at INTEGER NOT NULL, -- 记录创建时间,Unix 秒 UTC + updated_at INTEGER NOT NULL -- 记录最后更新时间,Unix 秒 UTC +); +""" + + +def _now_unix() -> int: + return int(time.time()) + + +def _unix_to_iso_local(ts: Optional[int]) -> Optional[str]: + """对外 JSON:本地时区 ISO8601 字符串,便于人读。""" + if ts is None: + return None + try: + return datetime.fromtimestamp(int(ts)).isoformat(timespec="seconds") + except (ValueError, OSError, OverflowError): + return None + + PLATFORM_URLS = { "sohu": "https://mp.sohu.com", "zhihu": "https://www.zhihu.com", @@ -75,107 +110,13 @@ def get_conn(): return sqlite3.connect(get_db_path()) -def _ensure_column(cur, table_name, col_name, ddl): - cur.execute(f"PRAGMA table_info({table_name})") - cols = [row[1] for row in cur.fetchall()] - if col_name not in cols: - cur.execute(f"ALTER TABLE {table_name} ADD COLUMN {col_name} {ddl}") - - -def _has_text_primary_id(cur): - cur.execute("PRAGMA table_info(accounts)") - for row in cur.fetchall(): - if row[1] == "id": - t = (row[2] or "").upper() - return "TEXT" in t or "CHAR" in t - return False - - -def _migrate_text_id_to_integer(conn): - cur = conn.cursor() - cur.execute( - """ - CREATE TABLE accounts_new ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - name TEXT NOT NULL, - platform TEXT NOT NULL, - phone TEXT, - profile_dir TEXT, - url TEXT, - login_status INTEGER DEFAULT 0, - last_login_at TEXT, - extra_json TEXT, - created_at TEXT NOT NULL, - updated_at TEXT NOT NULL - ) - """ - ) - cur.execute( - "SELECT name, platform, phone, profile_dir, login_status, last_login_at, extra_json, created_at, updated_at FROM accounts ORDER BY rowid" - ) - rows = cur.fetchall() - for row in rows: - name, platform, phone, profile_dir, login_status, last_login_at, extra_json, created_at, updated_at = row - plat = (platform or "").strip().lower() - url = PLATFORM_URLS.get(plat, "") - cur.execute( - """ - INSERT INTO accounts_new - (name, platform, phone, profile_dir, url, login_status, last_login_at, extra_json, created_at, updated_at) - VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?) - """, - ( - name, - plat, - phone or "", - profile_dir or "", - url, - int(login_status or 0), - last_login_at, - extra_json, - created_at, - updated_at, - ), - ) - cur.execute("DROP TABLE accounts") - cur.execute("ALTER TABLE accounts_new RENAME TO accounts") - - def init_db(): conn = get_conn() try: cur = conn.cursor() cur.execute("SELECT name FROM sqlite_master WHERE type='table' AND name='accounts'") if not cur.fetchone(): - cur.execute( - """ - CREATE TABLE accounts ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - name TEXT NOT NULL, - platform TEXT NOT NULL, - phone TEXT, - profile_dir TEXT, - url TEXT, - login_status INTEGER DEFAULT 0, - last_login_at TEXT, - extra_json TEXT, - created_at TEXT NOT NULL, - updated_at TEXT NOT NULL - ) - """ - ) - else: - if _has_text_primary_id(cur): - _migrate_text_id_to_integer(conn) - cur = conn.cursor() - _ensure_column(cur, "accounts", "url", "TEXT") - cur.execute( - "SELECT id, platform FROM accounts WHERE url IS NULL OR TRIM(COALESCE(url,'')) = ''" - ) - for rid, plat in cur.fetchall(): - pu = PLATFORM_URLS.get((plat or "").strip().lower(), "") - if pu: - cur.execute("UPDATE accounts SET url = ? WHERE id = ?", (pu, rid)) + cur.executescript(ACCOUNTS_TABLE_SQL) conn.commit() finally: conn.close() @@ -197,7 +138,11 @@ def get_account_by_id(account_id): try: cur = conn.cursor() cur.execute( - "SELECT id, name, platform, phone, profile_dir, url, login_status, last_login_at, extra_json FROM accounts WHERE id = ?", + """ + SELECT id, name, platform, phone, profile_dir, url, login_status, last_login_at, extra_json, + created_at, updated_at + FROM accounts WHERE id = ? + """, (aid,), ) row = cur.fetchone() @@ -211,7 +156,9 @@ def get_account_by_id(account_id): "profile_dir": row[4] or get_default_profile_dir(row[0]), "url": row[5] or PLATFORM_URLS.get(row[2], ""), "login_status": int(row[6] or 0), - "last_login_at": row[7], + "last_login_at": _unix_to_iso_local(row[7]), + "created_at": _unix_to_iso_local(row[9]), + "updated_at": _unix_to_iso_local(row[10]), } if row[8]: try: @@ -279,7 +226,7 @@ def cmd_add(platform: str, phone: str = ""): phone = (phone or "").strip() url = PLATFORM_URLS[platform] - now = datetime.now().isoformat(timespec="seconds") + now = _now_unix() conn = get_conn() try: @@ -298,7 +245,7 @@ def cmd_add(platform: str, phone: str = ""): profile_dir = get_default_profile_dir(new_id) cur.execute( "UPDATE accounts SET profile_dir = ?, updated_at = ? WHERE id = ?", - (profile_dir, now, new_id), + (profile_dir, _now_unix(), new_id), ) conn.commit() finally: @@ -363,6 +310,32 @@ def _login_timeout_seconds(): return 300 +def _login_poll_interval_seconds(): + """交互窗口内轮询地址栏的间隔,默认 1.5s,便于登录成功后尽快收尾。""" + try: + v = float((os.getenv("JIANGCHANG_LOGIN_POLL_INTERVAL_SECONDS") or "1.5").strip()) + return max(0.5, min(v, 10.0)) + except ValueError: + return 1.5 + + +def _url_looks_logged_in(platform: str, page_url: str) -> bool: + """根据当前 URL 判断是否已进入后台(与交互窗口、无头校验共用逻辑)。""" + u = (page_url or "").lower() + if not u: + return False + if platform == "sohu": + if "passport" in u or "/login" in u: + return False + # 登录成功后常见:…/mpfe/v4/contentManagement/… + if "mpfe" in u or "contentmanagement" in u: + return True + return False + if "login" in u or "signin" in u or "passport" in u: + return False + return True + + def _verify_login(profile_dir: str, platform: str, start_url: str, channel: str) -> bool: try: from playwright.sync_api import sync_playwright @@ -381,15 +354,12 @@ def _verify_login(profile_dir: str, platform: str, start_url: str, channel: str) try: page = ctx.pages[0] if ctx.pages else ctx.new_page() page.goto(start_url, wait_until="domcontentloaded", timeout=45000) - time.sleep(2) - u = (page.url or "").lower() - if platform == "sohu": - if "passport" in u or "/login" in u: - return False - return True - if "login" in u or "signin" in u or "passport" in u: - return False - return True + # 无头启动后可能仍在跳转,短轮询几次再判定 + for _ in range(20): + if _url_looks_logged_in(platform, page.url): + return True + time.sleep(1.0) + return _url_looks_logged_in(platform, page.url) finally: ctx.close() except Exception: @@ -418,18 +388,35 @@ def cmd_login(account_id): url = (target.get("url") or "").strip() or PLATFORM_URLS.get(target["platform"], "https://www.google.com") platform = (target.get("platform") or "").strip().lower() timeout_sec = _login_timeout_seconds() + poll_sec = _login_poll_interval_seconds() browser_name = "Google Chrome" if channel == "chrome" else "Microsoft Edge" print(f"正在为账号 [{target['name']}] 打开 {browser_name} …") print(f"访问地址:{url}") - print("请在窗口内完成登录(扫码/输入密码)。到达等待时间后将自动关闭窗口,您也可提前手动关闭。") - print(f"最长等待:{timeout_sec} 秒") + print( + "请在窗口内完成登录。系统会轮询地址栏,进入后台后将自动关闭窗口并写入状态(无需手动关浏览器)。" + ) + print(f"轮询间隔约 {poll_sec} 秒;全程最长 {timeout_sec} 秒。") - # Playwright 同步 API 宜在独立子进程中跑浏览器,避免线程与事件循环问题 - runner = r"""import json, sys + # 子进程:轮询 URL,已登录则立即关窗;勿仅用 wait_for_event(close),否则用户不关窗口会卡满整段超时。 + runner = r"""import json, sys, time from playwright.sync_api import sync_playwright + +def looks_sohu(u): + u = (u or "").lower() + if not u: + return False + if "passport" in u or "/login" in u: + return False + if "mpfe" in u or "contentmanagement" in u: + return True + return False + with open(sys.argv[1], encoding="utf-8") as f: c = json.load(f) +platform = (c.get("platform") or "").lower() +poll = float(c.get("poll_interval", 1.5)) +deadline = time.time() + float(c["timeout_sec"]) with sync_playwright() as p: ctx = p.chromium.launch_persistent_context( user_data_dir=c["profile_dir"], @@ -441,23 +428,36 @@ with sync_playwright() as p: try: page = ctx.pages[0] if ctx.pages else ctx.new_page() page.goto(c["url"], wait_until="domcontentloaded", timeout=60000) - try: - ctx.wait_for_event("close", timeout=c["timeout_sec"] * 1000) - except Exception: - pass + if platform == "sohu": + while time.time() < deadline: + try: + if looks_sohu(page.url): + break + except Exception: + break + time.sleep(poll) + else: + rem = max(0.0, deadline - time.time()) + try: + ctx.wait_for_event("close", timeout=rem * 1000) + except Exception: + pass + except Exception as e: + print(e, file=sys.stderr) + raise + finally: try: ctx.close() except Exception: pass - except Exception as e: - print(e, file=sys.stderr) - raise """ cfg = { "channel": channel, "profile_dir": profile_dir, "url": url, "timeout_sec": timeout_sec, + "platform": platform, + "poll_interval": poll_sec, } with tempfile.NamedTemporaryFile( mode="w", suffix=".json", delete=False, encoding="utf-8" @@ -499,7 +499,7 @@ with sync_playwright() as p: def _mark_login_status(account_id, success: bool): - now = datetime.now().isoformat(timespec="seconds") + now = _now_unix() conn = get_conn() try: cur = conn.cursor() From ba1c21c0b649e5ae329ee540fd6fc76d4a36d477 Mon Sep 17 00:00:00 2001 From: chendelian <116870791@qq.com> Date: Tue, 31 Mar 2026 18:37:04 +0800 Subject: [PATCH 27/27] chore: auto release commit (2026-03-31 18:37:04) --- SKILL.md | 12 +- scripts/__pycache__/account.cpython-312.pyc | Bin 25173 -> 32840 bytes scripts/account.py | 214 ++++++++++++++++++-- 3 files changed, 206 insertions(+), 20 deletions(-) diff --git a/SKILL.md b/SKILL.md index a3225d7..c821d85 100644 --- a/SKILL.md +++ b/SKILL.md @@ -1,7 +1,7 @@ --- name: 账号管理 description: 多平台多账号管理。管理各平台账号与Chrome Profile的对应关系,供publisher类Skill调用获取账号信息。 -version: 1.0.2 +version: 1.0.3 author: 深圳匠厂科技有限公司 metadata: openclaw: @@ -29,6 +29,16 @@ python3 {baseDir}/scripts/account.py add sohu python3 {baseDir}/scripts/account.py add sohu 189xxxxxxxx ``` +### 仅打开浏览器核对是否已登录(不写数据库) +```bash +python3 {baseDir}/scripts/account.py open +``` + +### 登录并自动检测、写入数据库 +```bash +python3 {baseDir}/scripts/account.py login +``` + ### 列出某平台所有账号 ```bash python3 {baseDir}/scripts/account.py list \ No newline at end of file diff --git a/scripts/__pycache__/account.cpython-312.pyc b/scripts/__pycache__/account.cpython-312.pyc index 4aef0a751a512ab5f334c1bbe02a6901ed7231cc..2acca904c278b3523adfb94c8b353533889da4d3 100644 GIT binary patch delta 10870 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 delta 5200 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 bool: + """ + 通用:是否「明显仍在登录/认证页」。 + 用路径段判断,避免 mpfe/v4/login 因含 mpfe 被误判为已登录。 + """ + if not page_url or not page_url.strip(): + return True + try: + p = urlparse(page_url.strip()) + except Exception: + return True + host = (p.netloc or "").lower() + path = (p.path or "").lower() + query = (p.query or "").lower() + if "passport" in host or "login." in host or "signin." in host: + return True + seg = [s for s in path.split("/") if s] + login_segments = ( + "login", + "signin", + "signup", + "register", + "authorize", + "authentication", + ) + if any(s in login_segments for s in seg): + return True + if re.search(r"/(login|signin)(/|$|\?)", path): + return True + if "redirect_uri" in query and ("login" in query or "passport" in query): + return True + return False + + +def _sohu_url_logged_in(page_url: str) -> bool: + """ + 搜狐:仅白名单路径视为已进入后台(不用裸 mpfe,避免 /mpfe/v4/login 误判)。 + """ + if _definitely_logged_out_url(page_url): + return False + try: + path = (urlparse(page_url).path or "").lower() + except Exception: + return False + if "contentmanagement" in path: + return True + if "/mpfe/v4/main" in path or path.rstrip("/").endswith("/main"): + return True + if "/mpfe/v4/publish" in path or "/mpfe/v4/data" in path: + return True + return False + + +def _sohu_page_shows_login_form(page) -> bool: + """搜狐登录页常见密码框;可见则认为未登录(与 URL 交叉验证)。""" + try: + loc = page.locator('input[type="password"]') + n = loc.count() + if n == 0: + return False + return loc.first.is_visible(timeout=800) + except Exception: + return False + + def _url_looks_logged_in(platform: str, page_url: str) -> bool: - """根据当前 URL 判断是否已进入后台(与交互窗口、无头校验共用逻辑)。""" - u = (page_url or "").lower() - if not u: + """是否判定为已登录:搜狐用白名单+负例;其它平台仅用「非明确登录页」宽松策略。""" + if not page_url or not page_url.strip(): return False if platform == "sohu": - if "passport" in u or "/login" in u: - return False - # 登录成功后常见:…/mpfe/v4/contentManagement/… - if "mpfe" in u or "contentmanagement" in u: - return True + return _sohu_url_logged_in(page_url) + if _definitely_logged_out_url(page_url): return False - if "login" in u or "signin" in u or "passport" in u: + u = page_url.lower() + if "passport" in u or "signin" in u: return False return True +def _verify_logged_in_page(platform: str, page) -> bool: + """结合 URL 与页面(搜狐加验登录表单)。""" + url = page.url or "" + if platform == "sohu": + if _sohu_page_shows_login_form(page): + return False + return _sohu_url_logged_in(url) + return _url_looks_logged_in(platform, url) + + def _verify_login(profile_dir: str, platform: str, start_url: str, channel: str) -> bool: try: from playwright.sync_api import sync_playwright @@ -354,12 +428,11 @@ def _verify_login(profile_dir: str, platform: str, start_url: str, channel: str) try: page = ctx.pages[0] if ctx.pages else ctx.new_page() page.goto(start_url, wait_until="domcontentloaded", timeout=45000) - # 无头启动后可能仍在跳转,短轮询几次再判定 for _ in range(20): - if _url_looks_logged_in(platform, page.url): + if _verify_logged_in_page(platform, page): return True time.sleep(1.0) - return _url_looks_logged_in(platform, page.url) + return _verify_logged_in_page(platform, page) finally: ctx.close() except Exception: @@ -398,20 +471,43 @@ def cmd_login(account_id): ) print(f"轮询间隔约 {poll_sec} 秒;全程最长 {timeout_sec} 秒。") - # 子进程:轮询 URL,已登录则立即关窗;勿仅用 wait_for_event(close),否则用户不关窗口会卡满整段超时。 + # 子进程:搜狐轮询 URL+页面;勿用「含 mpfe 即登录」,/mpfe/v4/login 会误判。 runner = r"""import json, sys, time +from urllib.parse import urlparse from playwright.sync_api import sync_playwright -def looks_sohu(u): - u = (u or "").lower() +def sohu_logged_in(url): + u = (url or "").strip() if not u: return False - if "passport" in u or "/login" in u: + try: + p = urlparse(u) + except Exception: return False - if "mpfe" in u or "contentmanagement" in u: + path = (p.path or "").lower() + host = (p.netloc or "").lower() + if "passport" in host: + return False + segs = [s for s in path.split("/") if s] + if any(s in ("login", "signin", "signup", "register", "authorize") for s in segs): + return False + if "contentmanagement" in path: + return True + if "/mpfe/v4/main" in path or path.rstrip("/").endswith("/main"): + return True + if "/mpfe/v4/publish" in path or "/mpfe/v4/data" in path: return True return False +def sohu_not_login_page(page): + try: + loc = page.locator('input[type="password"]') + if loc.count() == 0: + return False + return loc.first.is_visible(timeout=600) + except Exception: + return False + with open(sys.argv[1], encoding="utf-8") as f: c = json.load(f) platform = (c.get("platform") or "").lower() @@ -431,7 +527,7 @@ with sync_playwright() as p: if platform == "sohu": while time.time() < deadline: try: - if looks_sohu(page.url): + if sohu_logged_in(page.url) and not sohu_not_login_page(page): break except Exception: break @@ -498,6 +594,84 @@ with sync_playwright() as p: print("⚠️ 未检测到有效登录,状态为未登录。请关闭其他占用该用户目录的浏览器后重试,或延长 JIANGCHANG_LOGIN_TIMEOUT_SECONDS 后再登录。") +def cmd_open(account_id): + """打开该账号的持久化浏览器,仅用于肉眼确认是否已登录;不写数据库。""" + target = get_account_by_id(account_id) + if not target: + print("ERROR:ACCOUNT_NOT_FOUND") + return + + channel = resolve_chromium_channel() + if not channel: + _print_browser_install_hint() + return + + try: + import playwright # noqa: F401 + except ImportError: + print("ERROR:需要 playwright:pip install playwright && playwright install chromium") + return + + profile_dir = target["profile_dir"] + os.makedirs(profile_dir, exist_ok=True) + url = (target.get("url") or "").strip() or PLATFORM_URLS.get( + target["platform"], "https://www.google.com" + ) + browser_name = "Google Chrome" if channel == "chrome" else "Microsoft Edge" + print(f"正在打开 [{target['name']}] 的 {browser_name}(仅查看,不写入数据库)") + print(f"地址:{url}") + print("请在本窗口中自行确认登录态。关闭浏览器后命令结束。") + print("需要自动检测并写入数据库时,请执行:python account.py login ") + + runner_open = r"""import json, sys +from playwright.sync_api import sync_playwright +with open(sys.argv[1], encoding="utf-8") as f: + c = json.load(f) +with sync_playwright() as p: + ctx = p.chromium.launch_persistent_context( + user_data_dir=c["profile_dir"], + headless=False, + channel=c["channel"], + no_viewport=True, + args=["--start-maximized"], + ) + try: + page = ctx.pages[0] if ctx.pages else ctx.new_page() + page.goto(c["url"], wait_until="domcontentloaded", timeout=60000) + ctx.wait_for_event("close", timeout=86400000) + except Exception as e: + print(e, file=sys.stderr) + raise + finally: + try: + ctx.close() + except Exception: + pass +""" + cfg = {"channel": channel, "profile_dir": profile_dir, "url": url} + with tempfile.NamedTemporaryFile( + mode="w", suffix=".json", delete=False, encoding="utf-8" + ) as jf: + json.dump(cfg, jf, ensure_ascii=False) + cfg_path = jf.name + with tempfile.NamedTemporaryFile( + mode="w", suffix=".py", delete=False, encoding="utf-8" + ) as pf: + pf.write(runner_open) + py_path = pf.name + try: + subprocess.run([sys.executable, py_path, cfg_path]) + finally: + try: + os.unlink(cfg_path) + except OSError: + pass + try: + os.unlink(py_path) + except OSError: + pass + + def _mark_login_status(account_id, success: bool): now = _now_unix() conn = get_conn() @@ -521,7 +695,7 @@ def _mark_login_status(account_id, success: bool): if __name__ == "__main__": if len(sys.argv) < 2: print( - "用法:python account.py add [phone] | list [platform] | get | login " + "用法:python account.py add [phone] | list [platform] | get | open | login " ) sys.exit(1) @@ -538,6 +712,8 @@ if __name__ == "__main__": cmd_list(sys.argv[2] if len(sys.argv) >= 3 else "all") elif cmd == "get" and len(sys.argv) >= 3: cmd_get(sys.argv[2]) + elif cmd == "open" and len(sys.argv) >= 3: + cmd_open(sys.argv[2]) elif cmd == "login" and len(sys.argv) >= 3: cmd_login(sys.argv[2]) else: