Commit 6be2431a authored by 郭阔's avatar 郭阔

feat: login preflight before harvest

- nh_login_preflight (platform.sh): sources with credentials at
  $CONF_DIR/logins/<source>.json (600) get a login check via the
  skill's login-tools.js before claude runs; auto-login on failure;
  harvest never blocked (paywall honestly reported by result contract)
- harvest.sh invokes preflight after result contract preparation
- SKILLS_DIR / SKILLS_REPO_URL documented in default.conf.example
- test: preflight invoked when credentials configured (fake skill)
parent 776c3a9a
...@@ -60,6 +60,9 @@ cd "$WORK_DIR" ...@@ -60,6 +60,9 @@ cd "$WORK_DIR"
source "$SCRIPT_DIR/result.sh" source "$SCRIPT_DIR/result.sh"
prepare_result_contract || exit 1 prepare_result_contract || exit 1
# 登录预检:配置了凭据的源自动检查/登录其浏览器实例(失败不中断)
nh_login_preflight "$SOURCE"
START_EPOCH=$(date +%s) START_EPOCH=$(date +%s)
# --- 运行 claude 无头模式,输出 JSON --- # --- 运行 claude 无头模式,输出 JSON ---
......
...@@ -377,6 +377,34 @@ nh_skill_bootstrap() { # clone(可选)+ 依赖安装 + 全量 bundle 导入 + v ...@@ -377,6 +377,34 @@ nh_skill_bootstrap() { # clone(可选)+ 依赖安装 + 全量 bundle 导入 + v
nh_skill_configure_vault nh_skill_configure_vault
} }
# --- 登录预检(配置了凭据的浏览器源,采集前自动检查/登录) ---
# 凭据文件:$CONF_DIR/logins/<source>.json({"url":…,"user":…,"password":…},建议 600)。
# 配方与浏览器自动化在技能仓 login-tools.js;本层只做编排,登录失败不中断采集
# (结果契约会如实标记付费墙)。
nh_login_preflight() { # <source>
local source="$1" creds tools_dir
creds="$CONF_DIR/logins/${source}.json"
[ -f "$creds" ] || return 0
tools_dir="$(nh_skill_dir)"
if [ ! -f "$tools_dir/scripts/login-tools.js" ]; then
echo "⚠️ 已配置 $source 的登录凭据,但技能缺少 login-tools.js,跳过预检" >&2
return 0
fi
export NH_LOGIN_USER="$(jq -r '.user // empty' "$creds" 2>/dev/null)"
export NH_LOGIN_PASS="$(jq -r '.password // empty' "$creds" 2>/dev/null)"
if (cd "$tools_dir" && node scripts/login-tools.js check "$source") >/dev/null 2>&1; then
echo "ℹ️ 登录预检:$source 已登录"
else
echo "ℹ️ 登录预检:$source 未登录,自动登录…"
if (cd "$tools_dir" && node scripts/login-tools.js login "$source"); then
echo "✅ $source 自动登录成功"
else
echo "⚠️ $source 自动登录失败,继续采集(付费墙文章会被如实标记)" >&2
fi
fi
return 0
}
# --- CLI 派发(python 面板等外部调用):platform.sh call <函数> [参数...] --- # --- CLI 派发(python 面板等外部调用):platform.sh call <函数> [参数...] ---
# 例:platform.sh call nh_schedule_enable wsj # 例:platform.sh call nh_schedule_enable wsj
if [ "${1:-}" = "call" ]; then if [ "${1:-}" = "call" ]; then
......
...@@ -56,3 +56,7 @@ DIGEST_BUDGET_USD=2 ...@@ -56,3 +56,7 @@ DIGEST_BUDGET_USD=2
# LOG_DIR=/home/yichu/Documents/News-Harvester/logs # LOG_DIR=/home/yichu/Documents/News-Harvester/logs
# Obsidian Vault 根目录;设置后会拒绝读取该目录之外的 archive_path # Obsidian Vault 根目录;设置后会拒绝读取该目录之外的 archive_path
# OBSIDIAN_VAULT_PATH=/home/yichu/Documents/Obsidian/MyVault # OBSIDIAN_VAULT_PATH=/home/yichu/Documents/Obsidian/MyVault
# === 技能仓库(login 预检与采集调用) ===
# SKILLS_DIR=/Users/you/news-hub-skills
# SKILLS_REPO_URL=https://git.example.com/news-hub-skills.git # setup.sh 自动 clone 用
...@@ -375,6 +375,23 @@ class ScriptTests(TemporaryEnvironment): ...@@ -375,6 +375,23 @@ class ScriptTests(TemporaryEnvironment):
result = json.loads(next(self.logs.glob('*.json')).read_text()) result = json.loads(next(self.logs.glob('*.json')).read_text())
self.assertEqual(result['result']['failures'][0]['type'], 'INVALID_OUTPUT') self.assertEqual(result['result']['failures'][0]['type'], 'INVALID_OUTPUT')
def test_harvest_login_preflight_invoked(self):
"""配置了凭据的源,harvest 前自动调用技能的登录预检。"""
(self.conf / 'logins').mkdir()
(self.conf / 'logins' / 'sample.json').write_text('{"user":"u","password":"p"}')
scripts = self.base / 'skills' / 'skills' / 'news-harvester' / 'scripts'
scripts.mkdir(parents=True)
(scripts / 'login-tools.js').write_text(
"const fs=require('fs');const a=process.argv.slice(2);"
"fs.appendFileSync(process.env.NH_CALLS+'.login', a.join(' ')+'\\n');"
"process.exit(0);\n")
self.save_fixture(envelope([self.make_article('Full body text. ' * 120)]))
env = {**self.env, 'SKILLS_DIR': str(self.base / 'skills')}
process = subprocess.run(['bash', str(self.bin / 'harvest.sh'), 'sample', '1'],
env=env, text=True, capture_output=True, timeout=15)
self.assertEqual(process.returncode, 0, process.stderr)
self.assertIn('check sample', (self.base / 'calls.login').read_text())
def test_continue_and_repair_share_contract(self): def test_continue_and_repair_share_contract(self):
initial = result_module.normalize(envelope([self.make_article()]), 'sample', 1, 0, initial = result_module.normalize(envelope([self.make_article()]), 'sample', 1, 0,
self.base, self.base) self.base, self.base)
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment