Skip to content

Commit 757e9ef

Browse files
committed
Merge branch 'dev'
2 parents a8473b7 + 4b28e7a commit 757e9ef

11 files changed

Lines changed: 339 additions & 17 deletions

File tree

.gitignore

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,3 +29,10 @@ worktrees/
2929

3030
.phpunit.*
3131
captcha/data.json
32+
33+
# Local AI/Trellis tooling
34+
.ace-tool/
35+
.agents/
36+
.codex/
37+
.trellis/
38+
AGENTS.md

plugins/DynamicLottery/src/DynamicLotteryReservationExecutor.php

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,8 @@ protected function postJson(string $url, array $payload, array $headers): array
5858
{
5959
try {
6060
$raw = $this->request->postText('pc', $url, $payload, $headers);
61+
} catch (NoLoginException $exception) {
62+
throw $exception;
6163
} catch (Throwable $throwable) {
6264
return [
6365
'code' => -500,

plugins/Judge/src/JudgePlugin.php

Lines changed: 43 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,10 @@
1111

1212
class JudgePlugin extends BasePlugin implements PluginTaskInterface
1313
{
14+
private const VOTE_RESULT_SUCCESS = 'success';
15+
private const VOTE_RESULT_RETRY = 'retry';
16+
private const VOTE_RESULT_SKIP = 'skip';
17+
1418
private AuthFailureClassifier $authFailureClassifier;
1519
private ?ApiJury $juryApi = null;
1620

@@ -74,7 +78,8 @@ protected function judgementTask(): void
7478
return;
7579
}
7680

77-
if (!$this->vote($case['id'], $case['vote'])) {
81+
$voteResult = $this->vote($case['id'], $case['vote']);
82+
if ($voteResult === self::VOTE_RESULT_RETRY) {
7883
$this->scheduleAfter(60.0);
7984

8085
return;
@@ -106,10 +111,19 @@ protected function caseCheck(string $caseId): bool
106111

107112
$vote = $voteInfo['vote'];
108113
$voteText = $voteInfo['vote_text'];
109-
$this->wait_case[] = ['id' => $caseId, 'vote' => $vote];
110114
$this->info("風機委員: 案件{$caseId}的預測投票結果 {$vote}({$voteText})");
111-
$this->vote($caseId, 0);
112-
$this->scheduleAfter(65);
115+
116+
$voteResult = $this->vote($caseId, 0, 0);
117+
if ($voteResult === self::VOTE_RESULT_SUCCESS) {
118+
$this->wait_case[] = ['id' => $caseId, 'vote' => $vote];
119+
$this->scheduleAfter(65);
120+
121+
return false;
122+
}
123+
124+
if ($voteResult === self::VOTE_RESULT_RETRY) {
125+
$this->scheduleAfter(60.0);
126+
}
113127

114128
return false;
115129
}
@@ -118,20 +132,37 @@ protected function caseCheck(string $caseId): bool
118132
* 处理vote
119133
* @param string $caseId
120134
* @param int $vote
121-
* @return bool
135+
* @return self::VOTE_RESULT_*
122136
*/
123-
private function vote(string $caseId, int $vote): bool
137+
private function vote(string $caseId, int $vote, ?int $insiders = null): string
124138
{
125-
$response = $this->juryApi()->vote($caseId, $vote, '', 0, array_rand([0, 1]));
139+
$response = $this->juryApi()->vote($caseId, $vote, '', 0, $insiders ?? array_rand([0, 1]));
126140
$this->authFailureClassifier->assertNotAuthFailure($response, "風機委員: 案件{$caseId}投票时账号未登录");
127141

128-
if ($response['code']) {
129-
$this->warning("風機委員: 案件{$caseId}投票失败 {$response['code']} -> {$response['message']}");
130-
return false;
131-
} else {
142+
$code = (int)($response['code'] ?? -500);
143+
$message = trim((string)($response['message'] ?? ''));
144+
145+
if ($code === 0) {
132146
$this->notice("風機委員: 案件{$caseId}投票成功");
133-
return true;
147+
148+
return self::VOTE_RESULT_SUCCESS;
149+
}
150+
151+
$logMessage = "風機委員: 案件{$caseId}投票失败 {$code} -> {$message}";
152+
if ($this->isTerminalVoteFailure($code, $message)) {
153+
$this->warning($logMessage . ',跳过该案件');
154+
155+
return self::VOTE_RESULT_SKIP;
134156
}
157+
158+
$this->warning($logMessage);
159+
160+
return self::VOTE_RESULT_RETRY;
161+
}
162+
163+
private function isTerminalVoteFailure(int $code, string $message): bool
164+
{
165+
return in_array($code, [25009, 25018], true) || str_contains($message, '不能进行此操作');
135166
}
136167

137168
/**

src/App/AppKernel.php

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
use Bhp\Bootstrap\StartupSelfCheck;
77
use Bhp\Cache\Cache;
88
use Bhp\Config\Config;
9+
use Bhp\Console\Cli\RuntimeException as CliRuntimeException;
910
use Bhp\Console\Console;
1011
use Bhp\Console\Command\AppCommand;
1112
use Bhp\Console\Command\DebugCommand;
@@ -36,6 +37,7 @@
3637
use Bhp\Profile\ProfileCacheResetService;
3738
use Bhp\Profile\ProfileContext;
3839
use Bhp\Profile\ProfileInspector;
40+
use Bhp\Profile\ProfileRuntimeLock;
3941
use Bhp\Request\Request;
4042
use Bhp\Request\RequestRetryPolicy;
4143
use Bhp\Runtime\AppContext;
@@ -44,6 +46,7 @@
4446
use Bhp\Scheduler\Scheduler;
4547
use Bhp\Scheduler\SchedulerStateStore;
4648
use Bhp\WbiSign\WbiSign;
49+
use RuntimeException;
4750

4851
final class AppKernel
4952
{
@@ -72,10 +75,20 @@ public function boot(): BootstrapResult
7275
$profileName = Console::parse($this->argv);
7376
$runtimeMode = Console::resolveMode($this->argv);
7477
$profileContext = ProfileContext::fromAppRoot($this->appRoot, $profileName);
78+
$runtimeLock = new ProfileRuntimeLock($profileContext);
79+
if (!$readOnlyRequest) {
80+
try {
81+
$runtimeLock->acquire('mode:' . $runtimeMode);
82+
} catch (RuntimeException $exception) {
83+
throw new CliRuntimeException($exception->getMessage(), 0, $exception);
84+
}
85+
}
86+
7587
$container = new ServiceContainer();
7688

7789
$container->setInstance(ServiceContainer::class, $container);
7890
$container->setInstance(ProfileContext::class, $profileContext);
91+
$container->setInstance(ProfileRuntimeLock::class, $runtimeLock);
7992
$container->set(Core::class, static fn (ServiceContainer $services): Core => new Core($profileContext));
8093
$container->set(Config::class, static fn (ServiceContainer $services): Config => new Config($profileContext));
8194
$container->set(Cache::class, static fn (ServiceContainer $services): Cache => new Cache($profileContext));
@@ -190,7 +203,7 @@ public function boot(): BootstrapResult
190203
$this->argv,
191204
$profileContext->appRoot(),
192205
static fn (): Plugin => $services->get(Plugin::class),
193-
static fn (): ProfileCacheResetService => $services->get(ProfileCacheResetService::class)
206+
static fn (): ProfileCacheResetService => $services->get(ProfileCacheResetService::class),
194207
));
195208
$container->set(Console::class, fn (ServiceContainer $services): Console => new Console(
196209
$this->argv,

src/Cache/Cache.php

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,14 @@ public function flush(): void
9191
$this->store()->clear();
9292
}
9393

94+
/**
95+
* @return string[]
96+
*/
97+
public function scopes(): array
98+
{
99+
return $this->store()->scopes();
100+
}
101+
94102
/**
95103
* 处理ensureScopeInitialized
96104
* @param string $classname

src/Cache/CacheStoreInterface.php

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,11 @@ interface CacheStoreInterface
1010
*/
1111
public function databasePath(): string;
1212

13+
/**
14+
* @return string[]
15+
*/
16+
public function scopes(): array;
17+
1318
/**
1419
* 处理get
1520
* @param string $scope

src/Cache/SqliteCacheStore.php

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,52 @@ public function databasePath(): string
2525
return $this->databasePath;
2626
}
2727

28+
/**
29+
* @return string[]
30+
*/
31+
public function scopes(): array
32+
{
33+
if (!is_file($this->databasePath) && !$this->connection instanceof \SQLite3) {
34+
return [];
35+
}
36+
37+
$connection = null;
38+
$result = null;
39+
40+
try {
41+
$connection = $this->connection instanceof \SQLite3
42+
? $this->connection
43+
: $this->openExistingConnection();
44+
if (!$connection instanceof \SQLite3 || !$this->hasCacheEntriesTable($connection)) {
45+
return [];
46+
}
47+
48+
$result = $connection->query('SELECT DISTINCT scope FROM cache_entries ORDER BY scope');
49+
if (!$result instanceof \SQLite3Result) {
50+
return [];
51+
}
52+
53+
$scopes = [];
54+
while (($row = $result->fetchArray(SQLITE3_ASSOC)) !== false) {
55+
$scope = trim((string)($row['scope'] ?? ''));
56+
if ($scope !== '') {
57+
$scopes[] = $scope;
58+
}
59+
}
60+
61+
return $scopes;
62+
} catch (\Throwable) {
63+
return [];
64+
} finally {
65+
if ($result instanceof \SQLite3Result) {
66+
$result->finalize();
67+
}
68+
if ($connection instanceof \SQLite3 && $connection !== $this->connection) {
69+
$connection->close();
70+
}
71+
}
72+
}
73+
2874
/**
2975
* 处理get
3076
* @param string $scope
@@ -155,6 +201,39 @@ private function connection(): \SQLite3
155201
return $this->connection = $connection;
156202
}
157203

204+
private function openExistingConnection(): ?\SQLite3
205+
{
206+
if (!is_file($this->databasePath)) {
207+
return null;
208+
}
209+
210+
$connection = new \SQLite3($this->databasePath, SQLITE3_OPEN_READONLY);
211+
$connection->busyTimeout(5000);
212+
$connection->enableExceptions(true);
213+
214+
return $connection;
215+
}
216+
217+
private function hasCacheEntriesTable(\SQLite3 $connection): bool
218+
{
219+
$statement = $connection->prepare(
220+
"SELECT name FROM sqlite_master WHERE type = 'table' AND name = 'cache_entries' LIMIT 1"
221+
);
222+
if (!$statement instanceof \SQLite3Stmt) {
223+
return false;
224+
}
225+
226+
$result = $statement->execute();
227+
if (!$result instanceof \SQLite3Result) {
228+
return false;
229+
}
230+
231+
$row = $result->fetchArray(SQLITE3_ASSOC);
232+
$result->finalize();
233+
234+
return is_array($row) && ($row['name'] ?? '') === 'cache_entries';
235+
}
236+
158237
/**
159238
* 处理purgeEntries
160239
* @param \SQLite3 $connection

src/Config/ConfigTemplateSynchronizer.php

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ public function synchronize(string $templatePath, string $targetPath): ConfigTem
2424

2525
$backupPath = null;
2626
if ($targetExists) {
27-
$backupPath = $targetPath . '.bak';
27+
$backupPath = $this->nextBackupPath($targetPath);
2828
file_put_contents($backupPath, $targetContent);
2929
}
3030

@@ -128,6 +128,24 @@ private function detectNewline(string $content): string
128128
return "\n";
129129
}
130130

131+
private function nextBackupPath(string $targetPath): string
132+
{
133+
$timestamp = date('YmdHis');
134+
$basePath = $targetPath . '.' . $timestamp . '.bak';
135+
if (!is_file($basePath)) {
136+
return $basePath;
137+
}
138+
139+
for ($index = 1; $index < 1000; $index++) {
140+
$candidate = $targetPath . '.' . $timestamp . '.' . $index . '.bak';
141+
if (!is_file($candidate)) {
142+
return $candidate;
143+
}
144+
}
145+
146+
return $targetPath . '.' . $timestamp . '.' . bin2hex(random_bytes(4)) . '.bak';
147+
}
148+
131149
/**
132150
* @return string[]
133151
*/

src/Profile/ProfileCacheResetService.php

Lines changed: 29 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -40,19 +40,45 @@ private function clearCacheFiles(array $authSnapshot): void
4040
return;
4141
}
4242

43-
$this->cache->flush();
44-
$this->removeFiles([
43+
$files = [
4544
$cacheDir . DIRECTORY_SEPARATOR . 'cache.sqlite3',
4645
$cacheDir . DIRECTORY_SEPARATOR . 'cache.sqlite3-shm',
4746
$cacheDir . DIRECTORY_SEPARATOR . 'cache.sqlite3-wal',
48-
]);
47+
];
48+
$this->logResetPlan($files, $this->cache->scopes(), $authSnapshot !== []);
49+
50+
$this->cache->flush();
51+
$this->removeFiles($files);
4952

5053
if ($authSnapshot !== []) {
5154
$this->context->restoreAuthSnapshot($authSnapshot);
5255
$this->context->log()->recordInfo('已保留当前登录态');
5356
}
5457
}
5558

59+
/**
60+
* @param string[] $files
61+
* @param string[] $scopes
62+
*/
63+
private function logResetPlan(array $files, array $scopes, bool $keepAuth): void
64+
{
65+
$existingFiles = array_values(array_filter($files, 'is_file'));
66+
$this->context->log()->recordInfo(sprintf(
67+
'缓存重置: 登录态%s,持久 scope %d 个,缓存文件 %d 个',
68+
$keepAuth ? '保留' : '清理',
69+
count($scopes),
70+
count($existingFiles),
71+
));
72+
73+
if ($scopes !== []) {
74+
$this->context->log()->recordInfo('缓存重置 scope: ' . implode(', ', $scopes));
75+
}
76+
77+
foreach ($existingFiles as $file) {
78+
$this->context->log()->recordInfo('缓存重置文件: ' . basename($file));
79+
}
80+
}
81+
5682
/**
5783
* @param string[] $files
5884
*/

0 commit comments

Comments
 (0)