Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions DEVELOPMENT_LOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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` 全部通过。测试期间未调用真实平台,真实投稿严格限定为上述两条漏发任务。
8 changes: 8 additions & 0 deletions NEXT_STEPS.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 合并;不得自动合并。
12 changes: 9 additions & 3 deletions app/services/publishers/worker_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -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 (
Expand All @@ -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 "")
Expand Down Expand Up @@ -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:
Expand Down
2 changes: 1 addition & 1 deletion app/static/js/publish-center.js
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
2 changes: 1 addition & 1 deletion app/templates/publish.html
Original file line number Diff line number Diff line change
Expand Up @@ -164,7 +164,7 @@
<button type="button" class="text-button worker-refresh-button" data-refresh-worker>重新检测</button>
<small data-worker-message>{% if scheduler_health.consecutive_failures %}{{ scheduler_health.last_error_message }} · {% endif %}{{ scheduler_health.worker_message }} · 页面及排期均使用北京时间</small>
<div class="worker-connection-help" data-worker-help {% if scheduler_health.worker_available %}hidden{% endif %}>
<strong>自动连接:</strong><span>发送服务会在 Docker 中的牛马片场项目运行后自动启动。若刚刚运行项目,请稍候并点击“重新检测”;持续未连接时,请在 Docker Desktop 中停止后重新运行本项目。</span>
<strong>自动连接:</strong><span>发送服务由 RunDock 的 Niuma-Publish-Worker 独立托管。若刚刚启动项目,请稍候并点击“重新检测”;持续未连接时,请检查该注册项和本机 127.0.0.1:8765 监听状态。</span>
</div>
</section>

Expand Down
22 changes: 22 additions & 0 deletions scripts/native_environment.ps1
Original file line number Diff line number Diff line change
@@ -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 ',')
}
6 changes: 6 additions & 0 deletions scripts/run_native.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down Expand Up @@ -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
Expand All @@ -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 = @{}
Expand Down
6 changes: 6 additions & 0 deletions scripts/start_native.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down Expand Up @@ -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
Expand All @@ -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
Expand Down
29 changes: 29 additions & 0 deletions tests/test_native_scripts.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
import shutil
import subprocess
from pathlib import Path

import pytest


PROJECT_ROOT = Path(__file__).resolve().parents[1]

Expand Down Expand Up @@ -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():
Expand Down
7 changes: 5 additions & 2 deletions tests/test_publish_worker_autostart.py
Original file line number Diff line number Diff line change
Expand Up @@ -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


Expand Down
36 changes: 35 additions & 1 deletion tests/test_publish_worker_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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")
Expand Down Expand Up @@ -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


Expand Down
Loading