diff --git a/DEVELOPMENT_LOG.md b/DEVELOPMENT_LOG.md index ef3799e..e581ffb 100644 --- a/DEVELOPMENT_LOG.md +++ b/DEVELOPMENT_LOG.md @@ -1368,3 +1368,15 @@ - 强制用原始 `start_time` 后前 3 秒评价钩子,要求 15 秒内出现第一次有效刺激,并把评论动机、单一话题和完整反应闭环设为硬门槛。 - 片段以 60–90 秒为主,普通可看内容不得虚高到 78 分;本次不修改三阶段分析代码、A/B/C 门槛、候选池或历史分析结果。 - 相关选片与 Prompt 测试 `43 passed`,Ruff 和 `git diff --check` 通过;活动 SQLite 更新前已生成一致性备份,更新后 `integrity_check=ok`、外键异常为 0,其他 Prompt 与任务记录未变化。 + +## 2026-08-29 RunDock Worker 回环直连修复 + +- 定位到 RunDock 同时正常托管 `Niuma-Studio` 与 `Niuma-Publish-Worker`,但 Web 继承系统代理且缺少回环绕过,导致 `127.0.0.1:8765` 请求可能被代理返回 502;问题不是 Worker 未启动。 +- `PublishWorkerClient` 改用显式空代理 opener,所有健康检查、账号检查、登录、创作者中心、投稿与 execution 查询均不再继承系统代理;既有超时与结果不确定边界保持不变。 +- `start_native.ps1` 与 `run_native.ps1` 共用 `native_environment.ps1`,保留已有 `NO_PROXY` 条目、去重并追加 `127.0.0.1,localhost,::1`。 +- 发送中心与后端的旧 Docker Worker 提示改为当前 RunDock 原生双进程托管说明;Web 不会自行启动或合并独立 Worker 注册项。 +- 活动 SQLite 和 RunDock 状态在运行态调整前已备份;本次不修改数据库结构、公开 API、全局代理、AI Provider、账号配置或 E 盘媒体。 +- 仅为 Web 注册项补充 `NO_PROXY=127.0.0.1,localhost,::1` 并受控重启 Web;Worker 注册项和监听进程保持不变。现场复核 8001/8765 各只有一个监听者,调度器 `worker_available=true` 且 `last_scan_at` 连续推进。 +- 依次补发 `ea237f98f568` 与 `2059b5822678`,分别取得 execution `feee7d7789fb44fc8bce2b6a21ec2d11`、`bf7ae642f7d840588761c49879f9149b`;Worker 均为 `confirmed_success`,数据库均为 `PUBLISHED`,无 `NEED_REVIEW`、结果不确定或重复任务。 +- 抖音创作者中心独立核对:作品“小钟一上场就质疑代班主持:这集可以看吗”显示 2026-08-29 23:37 已发布,作品“小钟把英文歌唱成‘虾酥’,竟然真的接到代言”显示 2026-08-29 23:38 已发布;补发后仅剩原有 8 条未来排期。 +- 验证结果:定向测试 `23 passed`,完整测试 `803 passed`;Ruff、Compileall、PowerShell 5/7 静态解析和 `git diff --check` 全部通过。测试期间未调用真实平台,真实投稿严格限定为上述两条漏发任务。 diff --git a/NEXT_STEPS.md b/NEXT_STEPS.md index 1fc8210..8ede106 100644 --- a/NEXT_STEPS.md +++ b/NEXT_STEPS.md @@ -1125,3 +1125,11 @@ 2. 人工检查候选片段原始开头前 3 秒是否独立成立、15 秒内是否出现有效刺激,并优先保留 60–90 秒的完整互动回合。 3. 不要为了凑数开启普通片段;记录“不好笑、节奏拖沓、铺垫不足、内容重复”等反馈,供后续全局复审参考。 4. 连续发布 10 条后,在抖音后台与此前 10 条比较前段留存、平均观看、完播和评论表现;若无改善,再排查评分门槛、标题封面、账号分发或素材重复。 + +## 2026-08-29 RunDock Worker 回环与漏发验收(已完成) + +1. RunDock 中 `Niuma-Studio` 与 `Niuma-Publish-Worker` 均保持独立运行,8001/8765 各只有一个监听进程;Web 已补充回环 `NO_PROXY`,Worker 未重启、未合并注册项。 +2. 调度器已恢复为 `worker_available=true`,`last_scan_at` 连续推进;两条漏发任务分别取得独立 execution,Worker 均为 `confirmed_success`,数据库均为 `PUBLISHED`。 +3. 抖音创作者中心已逐条核对 23:37 与 23:38 两条作品,状态均为“已发布”;发送中心只剩 8 条未来排期,没有新建重复任务。 +4. 下一步仅需观察下一条未来排期是否按原计划执行;若以后出现登录、验证码、风控、超时或结果不确定,继续按 `NEED_REVIEW` 规则停止自动重试并人工核对。 +5. 本修复通过 PR 评审和 CI 后再由用户决定是否 Squash 合并;不得自动合并。 diff --git a/app/services/publishers/worker_client.py b/app/services/publishers/worker_client.py index 56db7d2..80e6685 100644 --- a/app/services/publishers/worker_client.py +++ b/app/services/publishers/worker_client.py @@ -7,7 +7,7 @@ import socket from typing import Any from urllib.error import HTTPError, URLError -from urllib.request import Request, urlopen +from urllib.request import ProxyHandler, Request, build_opener from app.core.config import settings from app.services.publishers.base import ( @@ -26,6 +26,12 @@ } +def urlopen(request: Request, timeout: int): + """直连本机 Worker,不继承宿主进程的系统代理。""" + opener = build_opener(ProxyHandler({})) + return opener.open(request, timeout=timeout) # noqa: S310 - Worker URL is local configuration + + def validate_worker_identifier(value: str, field_name: str, *, max_length: int) -> str: """验证会进入 URL、journal 或 Windows 目录名的稳定标识。""" text = str(value or "") @@ -92,8 +98,8 @@ def _request(self, method: str, path: str, payload: dict[str, Any] | None = None and not definitely_not_connected ) raise PublishWorkerUnavailable( - "发送服务正在随 Docker 中的牛马片场项目自动启动。" - "如果刚刚运行项目,请稍候并重新检测;持续未连接时,请在 Docker Desktop 中停止后重新运行本项目。", + "Windows 发布 Worker 未连接。请确认 RunDock 中独立托管的 " + "Niuma-Publish-Worker 正在运行,并检查本机 127.0.0.1:8765 监听状态。", request_may_have_been_received=timed_out or publish_result_uncertain, ) from exc if not raw: diff --git a/app/static/js/publish-center.js b/app/static/js/publish-center.js index 1c656f6..7325707 100644 --- a/app/static/js/publish-center.js +++ b/app/static/js/publish-center.js @@ -1442,7 +1442,7 @@ if (publishCenterRoot) { const ready = schedulerHealthy && data.worker_available; const resultMessage = schedulerFailures ? (data.last_error_message || "调度扫描异常,正在自动重试") - : (data.worker_available ? "调度器与 Windows Worker 均已连接。" : "发送服务仍在随 Docker 项目自动启动;请稍候,或在 Docker Desktop 中停止后重新运行本项目。"); + : (data.worker_available ? "调度器与 Windows Worker 均已连接。" : "发送服务由 RunDock 的 Niuma-Publish-Worker 独立托管;请检查该注册项和本机 127.0.0.1:8765 监听状态。"); showMessage(resultMessage, ready ? "success" : "error"); } } catch (error) { diff --git a/app/templates/publish.html b/app/templates/publish.html index ba870cf..2f57bbd 100644 --- a/app/templates/publish.html +++ b/app/templates/publish.html @@ -164,7 +164,7 @@ {% if scheduler_health.consecutive_failures %}{{ scheduler_health.last_error_message }} · {% endif %}{{ scheduler_health.worker_message }} · 页面及排期均使用北京时间
- 自动连接:发送服务会在 Docker 中的牛马片场项目运行后自动启动。若刚刚运行项目,请稍候并点击“重新检测”;持续未连接时,请在 Docker Desktop 中停止后重新运行本项目。 + 自动连接:发送服务由 RunDock 的 Niuma-Publish-Worker 独立托管。若刚刚启动项目,请稍候并点击“重新检测”;持续未连接时,请检查该注册项和本机 127.0.0.1:8765 监听状态。
diff --git a/scripts/native_environment.ps1 b/scripts/native_environment.ps1 new file mode 100644 index 0000000..e576cb6 --- /dev/null +++ b/scripts/native_environment.ps1 @@ -0,0 +1,22 @@ +function Merge-NativeNoProxy { + [CmdletBinding()] + param( + [AllowNull()] + [AllowEmptyString()] + [string[]]$ExistingValues = @() + ) + + $merged = @() + $seen = @{} + foreach ($value in @($ExistingValues) + @('127.0.0.1', 'localhost', '::1')) { + foreach ($entry in ([string]$value -split ',')) { + $candidate = $entry.Trim() + if ([string]::IsNullOrWhiteSpace($candidate) -or $seen.ContainsKey($candidate)) { + continue + } + $seen[$candidate] = $true + $merged += $candidate + } + } + return ($merged -join ',') +} diff --git a/scripts/run_native.ps1 b/scripts/run_native.ps1 index c0cc18d..98e0a93 100644 --- a/scripts/run_native.ps1 +++ b/scripts/run_native.ps1 @@ -12,6 +12,7 @@ param( $ErrorActionPreference = 'Stop' $ProjectRoot = (Resolve-Path (Join-Path $PSScriptRoot '..')).Path.TrimEnd('\') Set-Location -LiteralPath $ProjectRoot +. (Join-Path $PSScriptRoot 'native_environment.ps1') function Resolve-NativePath { param( @@ -51,6 +52,10 @@ if ($databaseParent) { New-Item -ItemType Directory -Path $databaseParent -Force | Out-Null } +$NativeNoProxy = Merge-NativeNoProxy -ExistingValues @( + [Environment]::GetEnvironmentVariable('NO_PROXY', 'Process'), + [Environment]::GetEnvironmentVariable('no_proxy', 'Process') +) $nativeEnvironment = [ordered]@{ DATA_DIR = $DataDirPath DATABASE_PATH = $DatabasePathPath @@ -62,6 +67,7 @@ $nativeEnvironment = [ordered]@{ PUBLISH_WORKER_URL = "http://127.0.0.1:$WorkerPort" OPENCLI_HOST_BRIDGE_URL = "http://127.0.0.1:$WorkerPort" OPENCLI_LOCAL_BASE_URL = "http://127.0.0.1:$Port" + NO_PROXY = $NativeNoProxy } $previousEnvironment = @{} diff --git a/scripts/start_native.ps1 b/scripts/start_native.ps1 index 7f0d3e6..05b2039 100644 --- a/scripts/start_native.ps1 +++ b/scripts/start_native.ps1 @@ -14,6 +14,7 @@ param( $ErrorActionPreference = 'Stop' $ProjectRoot = (Resolve-Path (Join-Path $PSScriptRoot '..')).Path.TrimEnd('\') Set-Location -LiteralPath $ProjectRoot +. (Join-Path $PSScriptRoot 'native_environment.ps1') function Resolve-NativePath { param( @@ -117,6 +118,10 @@ if (-not $SkipWorker) { } } +$NativeNoProxy = Merge-NativeNoProxy -ExistingValues @( + [Environment]::GetEnvironmentVariable('NO_PROXY', 'Process'), + [Environment]::GetEnvironmentVariable('no_proxy', 'Process') +) $nativeEnvironment = [ordered]@{ DATA_DIR = $DataDirPath DATABASE_PATH = $DatabasePathPath @@ -128,6 +133,7 @@ $nativeEnvironment = [ordered]@{ PUBLISH_WORKER_URL = "http://127.0.0.1:$WorkerPort" OPENCLI_HOST_BRIDGE_URL = "http://127.0.0.1:$WorkerPort" OPENCLI_LOCAL_BASE_URL = "http://127.0.0.1:$Port" + NO_PROXY = $NativeNoProxy } $shellCommand = Get-Command powershell.exe -ErrorAction SilentlyContinue diff --git a/tests/test_native_scripts.py b/tests/test_native_scripts.py index b3695cb..804e0e1 100644 --- a/tests/test_native_scripts.py +++ b/tests/test_native_scripts.py @@ -1,5 +1,9 @@ +import shutil +import subprocess from pathlib import Path +import pytest + PROJECT_ROOT = Path(__file__).resolve().parents[1] @@ -61,6 +65,31 @@ def test_native_scripts_set_host_paths_and_loopback_urls_without_env_file_access assert ".env" not in runner assert "compose" not in start.lower() assert "compose" not in runner.lower() + for script in (start, runner): + assert "native_environment.ps1" in script + assert "Merge-NativeNoProxy" in script + assert "NO_PROXY = $NativeNoProxy" in script + + +def test_native_no_proxy_merge_preserves_existing_entries_and_adds_loopback(): + shell = shutil.which("powershell.exe") or shutil.which("pwsh") + if not shell: + pytest.skip("当前环境没有 PowerShell") + helper = (PROJECT_ROOT / "scripts" / "native_environment.ps1").resolve() + escaped_helper = str(helper).replace("'", "''") + command = ( + f". '{escaped_helper}'; " + "Merge-NativeNoProxy -ExistingValues " + "@('corp.internal,localhost', 'api.local,127.0.0.1')" + ) + result = subprocess.run( + [shell, "-NoLogo", "-NoProfile", "-NonInteractive", "-Command", command], + check=True, + capture_output=True, + text=True, + ) + + assert result.stdout.strip() == "corp.internal,localhost,api.local,127.0.0.1,::1" def test_native_stop_requires_state_identity_and_only_stops_owned_worker(): diff --git a/tests/test_publish_worker_autostart.py b/tests/test_publish_worker_autostart.py index f56fa93..f68ef7a 100644 --- a/tests/test_publish_worker_autostart.py +++ b/tests/test_publish_worker_autostart.py @@ -56,8 +56,11 @@ def test_publish_center_no_longer_requests_manual_start_command(): worker_client = _read("app/services/publishers/worker_client.py") assert r".\scripts\start_niuma_studio.ps1" not in template - assert "发送服务会在 Docker 中的牛马片场项目运行后自动启动" in template - assert "随 Docker 项目自动启动" in javascript + assert "RunDock 的 Niuma-Publish-Worker 独立托管" in template + assert "RunDock 的 Niuma-Publish-Worker 独立托管" in javascript + assert "Docker Desktop" not in template + assert "随 Docker 项目自动启动" not in javascript + assert "RunDock 中独立托管" in worker_client assert r".\scripts\start_niuma_studio.ps1" not in worker_client diff --git a/tests/test_publish_worker_client.py b/tests/test_publish_worker_client.py index 2f5cdd0..2c5a85d 100644 --- a/tests/test_publish_worker_client.py +++ b/tests/test_publish_worker_client.py @@ -2,6 +2,8 @@ import json import socket +from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer +from threading import Thread import pytest from fastapi.testclient import TestClient @@ -79,6 +81,37 @@ def fake_urlopen(request, timeout): assert captured == {"authorization": "Bearer secret-token", "timeout": 7} +def test_worker_client_bypasses_environment_proxy_without_no_proxy(monkeypatch): + class HealthHandler(BaseHTTPRequestHandler): + def do_GET(self): + body = json.dumps({"status": "ok", "worker": "windows_chrome"}).encode("utf-8") + self.send_response(200) + self.send_header("Content-Type", "application/json") + self.send_header("Content-Length", str(len(body))) + self.end_headers() + self.wfile.write(body) + + def log_message(self, *_): + return + + for name in ("HTTP_PROXY", "HTTPS_PROXY", "ALL_PROXY", "http_proxy", "https_proxy", "all_proxy"): + monkeypatch.setenv(name, "http://127.0.0.1:1") + for name in ("NO_PROXY", "no_proxy"): + monkeypatch.delenv(name, raising=False) + + server = ThreadingHTTPServer(("127.0.0.1", 0), HealthHandler) + thread = Thread(target=server.serve_forever, daemon=True) + thread.start() + try: + result = PublishWorkerClient(f"http://127.0.0.1:{server.server_port}", "token", 2).health() + finally: + server.shutdown() + server.server_close() + thread.join(timeout=2) + + assert result == {"status": "ok", "worker": "windows_chrome"} + + def test_worker_timeout_is_marked_as_possibly_received(monkeypatch): def timeout(*_, **__): raise socket.timeout("timed out") @@ -112,7 +145,8 @@ def offline(*_, **__): with pytest.raises(PublishWorkerUnavailable) as caught: PublishWorkerClient("http://127.0.0.1:8765", "token", 2).health() assert caught.value.request_may_have_been_received is False - assert "随 Docker 中的牛马片场项目自动启动" in caught.value.message + assert "RunDock 中独立托管" in caught.value.message + assert "127.0.0.1:8765" in caught.value.message assert r".\scripts" not in caught.value.message