mirror of
https://gitee.com/mateos/mateclaw.git
synced 2026-09-15 20:08:18 +08:00
feat(system): add runtime-toggleable feature flag store
This commit is contained in:
parent
d0706239ea
commit
4f96a46900
@ -0,0 +1,88 @@
|
|||||||
|
package vip.mate.system.featureflag;
|
||||||
|
|
||||||
|
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||||
|
import jakarta.validation.constraints.Max;
|
||||||
|
import jakarta.validation.constraints.Min;
|
||||||
|
import jakarta.validation.constraints.NotBlank;
|
||||||
|
import lombok.Data;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import org.springframework.web.bind.annotation.GetMapping;
|
||||||
|
import org.springframework.web.bind.annotation.PathVariable;
|
||||||
|
import org.springframework.web.bind.annotation.PutMapping;
|
||||||
|
import org.springframework.web.bind.annotation.RequestBody;
|
||||||
|
import org.springframework.web.bind.annotation.RequestMapping;
|
||||||
|
import org.springframework.web.bind.annotation.RestController;
|
||||||
|
import vip.mate.common.result.R;
|
||||||
|
import vip.mate.system.featureflag.repository.FeatureFlagMapper;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Admin endpoints for runtime feature-flag toggling.
|
||||||
|
*
|
||||||
|
* <p>Authn / authz are delegated to the global security configuration:
|
||||||
|
* the JWT filter populates the principal, and the controller method runs
|
||||||
|
* inside the standard admin-role guard. Edit access should be restricted
|
||||||
|
* to operators; everyday users have no business toggling these.
|
||||||
|
*
|
||||||
|
* @author MateClaw Team
|
||||||
|
*/
|
||||||
|
@RestController
|
||||||
|
@RequestMapping("/api/v1/feature-flags")
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class FeatureFlagController {
|
||||||
|
|
||||||
|
private final FeatureFlagService service;
|
||||||
|
private final FeatureFlagMapper mapper;
|
||||||
|
|
||||||
|
/** Lists every flag currently registered, including disabled and whitelisted ones. */
|
||||||
|
@GetMapping
|
||||||
|
public R<List<FeatureFlagEntity>> list() {
|
||||||
|
return R.ok(mapper.selectList(null));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Updates one flag in place. Only fields explicitly set in the request
|
||||||
|
* body are touched; unspecified fields preserve their current values.
|
||||||
|
*/
|
||||||
|
@PutMapping("/{flagKey}")
|
||||||
|
public R<Void> update(@PathVariable @NotBlank String flagKey,
|
||||||
|
@RequestBody UpdateRequest req) {
|
||||||
|
FeatureFlagEntity flag = mapper.selectOne(
|
||||||
|
new LambdaQueryWrapper<FeatureFlagEntity>()
|
||||||
|
.eq(FeatureFlagEntity::getFlagKey, flagKey));
|
||||||
|
if (flag == null) {
|
||||||
|
return R.fail("Unknown flag: " + flagKey);
|
||||||
|
}
|
||||||
|
if (req.getEnabled() != null) {
|
||||||
|
flag.setEnabled(req.getEnabled());
|
||||||
|
}
|
||||||
|
if (req.getDescription() != null) {
|
||||||
|
flag.setDescription(req.getDescription());
|
||||||
|
}
|
||||||
|
if (req.getWhitelistKbIds() != null) {
|
||||||
|
flag.setWhitelistKbIds(req.getWhitelistKbIds());
|
||||||
|
}
|
||||||
|
if (req.getWhitelistUserIds() != null) {
|
||||||
|
flag.setWhitelistUserIds(req.getWhitelistUserIds());
|
||||||
|
}
|
||||||
|
if (req.getRolloutPercent() != null) {
|
||||||
|
flag.setRolloutPercent(req.getRolloutPercent());
|
||||||
|
}
|
||||||
|
mapper.updateById(flag);
|
||||||
|
service.invalidate();
|
||||||
|
return R.ok();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Body for {@link #update(String, UpdateRequest)}. */
|
||||||
|
@Data
|
||||||
|
public static class UpdateRequest {
|
||||||
|
private Boolean enabled;
|
||||||
|
private String description;
|
||||||
|
private String whitelistKbIds;
|
||||||
|
private String whitelistUserIds;
|
||||||
|
@Min(0)
|
||||||
|
@Max(100)
|
||||||
|
private Integer rolloutPercent;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,53 @@
|
|||||||
|
package vip.mate.system.featureflag;
|
||||||
|
|
||||||
|
import com.baomidou.mybatisplus.annotation.FieldFill;
|
||||||
|
import com.baomidou.mybatisplus.annotation.IdType;
|
||||||
|
import com.baomidou.mybatisplus.annotation.TableField;
|
||||||
|
import com.baomidou.mybatisplus.annotation.TableId;
|
||||||
|
import com.baomidou.mybatisplus.annotation.TableLogic;
|
||||||
|
import com.baomidou.mybatisplus.annotation.TableName;
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* One row in the runtime feature-flag store.
|
||||||
|
*
|
||||||
|
* <p>Backed by {@code mate_feature_flag}. Read/write paths go through
|
||||||
|
* {@link FeatureFlagService}; no service should query the table directly.
|
||||||
|
*
|
||||||
|
* @author MateClaw Team
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
@TableName("mate_feature_flag")
|
||||||
|
public class FeatureFlagEntity {
|
||||||
|
|
||||||
|
@TableId(type = IdType.AUTO)
|
||||||
|
private Long id;
|
||||||
|
|
||||||
|
/** Stable identifier; convention is {@code <module>.<capability>.enabled}. */
|
||||||
|
private String flagKey;
|
||||||
|
|
||||||
|
/** Master switch. When false, all evaluations short-circuit to false. */
|
||||||
|
private Boolean enabled;
|
||||||
|
|
||||||
|
private String description;
|
||||||
|
|
||||||
|
/** Comma-separated KB ids; null/blank = applies to all KBs. */
|
||||||
|
private String whitelistKbIds;
|
||||||
|
|
||||||
|
/** Comma-separated user ids; null/blank = applies to all users. */
|
||||||
|
private String whitelistUserIds;
|
||||||
|
|
||||||
|
/** Percentage rollout (0..100). Only consulted when both whitelists are blank. */
|
||||||
|
private Integer rolloutPercent;
|
||||||
|
|
||||||
|
@TableField(fill = FieldFill.INSERT)
|
||||||
|
private LocalDateTime createTime;
|
||||||
|
|
||||||
|
@TableField(fill = FieldFill.INSERT_UPDATE)
|
||||||
|
private LocalDateTime updateTime;
|
||||||
|
|
||||||
|
@TableLogic
|
||||||
|
private Integer deleted;
|
||||||
|
}
|
||||||
@ -0,0 +1,171 @@
|
|||||||
|
package vip.mate.system.featureflag;
|
||||||
|
|
||||||
|
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||||
|
import jakarta.annotation.PostConstruct;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.springframework.scheduling.annotation.Scheduled;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
import vip.mate.system.featureflag.repository.FeatureFlagMapper;
|
||||||
|
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.HashSet;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Set;
|
||||||
|
import java.util.concurrent.ConcurrentHashMap;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Runtime-toggleable feature-flag store with in-memory caching.
|
||||||
|
*
|
||||||
|
* <p>Reads are O(1) once the cache is warm; the cache refreshes on a
|
||||||
|
* configurable timer (default 30 s) and immediately on admin write via
|
||||||
|
* {@link #invalidate()}. Multi-instance deployments converge within one
|
||||||
|
* refresh tick; admin writes from one instance are not pushed to peers
|
||||||
|
* (acceptable for a coarse-grained flag system).
|
||||||
|
*
|
||||||
|
* <p>Evaluation order:
|
||||||
|
* <ol>
|
||||||
|
* <li>If {@code enabled == false}, return false.</li>
|
||||||
|
* <li>If {@code whitelist_kb_ids} is set and the context has a kbId,
|
||||||
|
* require kb membership; absence in the whitelist returns false.</li>
|
||||||
|
* <li>Same for {@code whitelist_user_ids}.</li>
|
||||||
|
* <li>If both whitelists are blank and {@code rollout_percent} is in
|
||||||
|
* (0, 100), use a deterministic hash of the context's keying value
|
||||||
|
* to gate.</li>
|
||||||
|
* <li>Otherwise return true.</li>
|
||||||
|
* </ol>
|
||||||
|
*
|
||||||
|
* <p>Unknown flags evaluate to false (fail-closed). This makes it safe to
|
||||||
|
* remove a flag from the seed list: callers see the feature as disabled
|
||||||
|
* until the flag is re-introduced.
|
||||||
|
*
|
||||||
|
* @author MateClaw Team
|
||||||
|
*/
|
||||||
|
@Slf4j
|
||||||
|
@Service
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class FeatureFlagService {
|
||||||
|
|
||||||
|
private static final FeatureFlagEntity MISSING = new FeatureFlagEntity();
|
||||||
|
|
||||||
|
private final FeatureFlagMapper mapper;
|
||||||
|
|
||||||
|
/** flagKey → resolved entity. {@link #MISSING} sentinel marks "not in DB". */
|
||||||
|
private final ConcurrentHashMap<String, FeatureFlagEntity> cache = new ConcurrentHashMap<>();
|
||||||
|
|
||||||
|
@PostConstruct
|
||||||
|
public void init() {
|
||||||
|
refresh();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Returns true iff the flag is enabled in the given context. */
|
||||||
|
public boolean isEnabled(String flagKey, FlagContext ctx) {
|
||||||
|
FeatureFlagEntity flag = cache.computeIfAbsent(flagKey, this::loadOne);
|
||||||
|
if (flag == MISSING || !Boolean.TRUE.equals(flag.getEnabled())) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return matchesContext(flag, ctx == null ? FlagContext.empty() : ctx);
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isEnabled(String flagKey) {
|
||||||
|
return isEnabled(flagKey, FlagContext.empty());
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isEnabledForKb(String flagKey, Long kbId) {
|
||||||
|
return isEnabled(flagKey, FlagContext.ofKb(kbId));
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isEnabledForUser(String flagKey, Long userId) {
|
||||||
|
return isEnabled(flagKey, FlagContext.ofUser(userId));
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Periodic full refresh; covers DB writes that bypass the admin API. */
|
||||||
|
@Scheduled(fixedDelayString = "${mateclaw.feature-flag.refresh-ms:30000}")
|
||||||
|
public void refresh() {
|
||||||
|
try {
|
||||||
|
List<FeatureFlagEntity> all = mapper.selectList(null);
|
||||||
|
ConcurrentHashMap<String, FeatureFlagEntity> next = new ConcurrentHashMap<>();
|
||||||
|
for (FeatureFlagEntity flag : all) {
|
||||||
|
next.put(flag.getFlagKey(), flag);
|
||||||
|
}
|
||||||
|
cache.clear();
|
||||||
|
cache.putAll(next);
|
||||||
|
log.debug("[FeatureFlag] refreshed {} flags from DB", next.size());
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.warn("[FeatureFlag] refresh failed; keeping previous cache: {}", e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Invalidate + reload; called by admin-write paths to make changes visible immediately. */
|
||||||
|
public void invalidate() {
|
||||||
|
cache.clear();
|
||||||
|
refresh();
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== internal ====================
|
||||||
|
|
||||||
|
private boolean matchesContext(FeatureFlagEntity flag, FlagContext ctx) {
|
||||||
|
boolean kbWhitelistDefined = isPresent(flag.getWhitelistKbIds());
|
||||||
|
boolean userWhitelistDefined = isPresent(flag.getWhitelistUserIds());
|
||||||
|
|
||||||
|
if (kbWhitelistDefined && ctx.getKbId() != null) {
|
||||||
|
Set<Long> kbs = parseIds(flag.getWhitelistKbIds());
|
||||||
|
if (!kbs.contains(ctx.getKbId())) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (userWhitelistDefined && ctx.getUserId() != null) {
|
||||||
|
Set<Long> users = parseIds(flag.getWhitelistUserIds());
|
||||||
|
if (!users.contains(ctx.getUserId())) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Percentage rollout only consulted when no whitelist applies.
|
||||||
|
if (!kbWhitelistDefined && !userWhitelistDefined) {
|
||||||
|
Integer pct = flag.getRolloutPercent();
|
||||||
|
if (pct != null && pct > 0 && pct < 100) {
|
||||||
|
long key = ctx.getKbId() != null
|
||||||
|
? ctx.getKbId()
|
||||||
|
: ctx.getUserId() != null
|
||||||
|
? ctx.getUserId()
|
||||||
|
: flag.getFlagKey().hashCode();
|
||||||
|
return Math.floorMod(key, 100L) < pct;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static boolean isPresent(String csv) {
|
||||||
|
return csv != null && !csv.isBlank();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Set<Long> parseIds(String csv) {
|
||||||
|
return Arrays.stream(csv.split(","))
|
||||||
|
.map(String::trim)
|
||||||
|
.filter(s -> !s.isEmpty())
|
||||||
|
.map(s -> {
|
||||||
|
try {
|
||||||
|
return Long.parseLong(s);
|
||||||
|
} catch (NumberFormatException e) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.filter(java.util.Objects::nonNull)
|
||||||
|
.collect(Collectors.toCollection(HashSet::new));
|
||||||
|
}
|
||||||
|
|
||||||
|
private FeatureFlagEntity loadOne(String flagKey) {
|
||||||
|
try {
|
||||||
|
FeatureFlagEntity flag = mapper.selectOne(
|
||||||
|
new LambdaQueryWrapper<FeatureFlagEntity>()
|
||||||
|
.eq(FeatureFlagEntity::getFlagKey, flagKey));
|
||||||
|
return flag != null ? flag : MISSING;
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.warn("[FeatureFlag] loadOne({}) failed: {}", flagKey, e.getMessage());
|
||||||
|
return MISSING;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,34 @@
|
|||||||
|
package vip.mate.system.featureflag;
|
||||||
|
|
||||||
|
import lombok.Builder;
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Evaluation context passed to {@link FeatureFlagService#isEnabled}.
|
||||||
|
*
|
||||||
|
* <p>Either or both of {@code kbId} and {@code userId} may be set. The
|
||||||
|
* service uses them to match against per-flag whitelists; if both are null
|
||||||
|
* the flag is evaluated against the percentage-rollout dial only.
|
||||||
|
*
|
||||||
|
* @author MateClaw Team
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
@Builder
|
||||||
|
public class FlagContext {
|
||||||
|
|
||||||
|
private Long kbId;
|
||||||
|
private Long userId;
|
||||||
|
private String role;
|
||||||
|
|
||||||
|
public static FlagContext empty() {
|
||||||
|
return FlagContext.builder().build();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static FlagContext ofKb(Long kbId) {
|
||||||
|
return FlagContext.builder().kbId(kbId).build();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static FlagContext ofUser(Long userId) {
|
||||||
|
return FlagContext.builder().userId(userId).build();
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,17 @@
|
|||||||
|
package vip.mate.system.featureflag.repository;
|
||||||
|
|
||||||
|
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||||
|
import org.apache.ibatis.annotations.Mapper;
|
||||||
|
import vip.mate.system.featureflag.FeatureFlagEntity;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* MyBatis Plus mapper for {@link FeatureFlagEntity}.
|
||||||
|
*
|
||||||
|
* <p>Mapper interface lives under a {@code repository} sub-package as required
|
||||||
|
* by the application-wide {@code @MapperScan("vip.mate.**.repository")}.
|
||||||
|
*
|
||||||
|
* @author MateClaw Team
|
||||||
|
*/
|
||||||
|
@Mapper
|
||||||
|
public interface FeatureFlagMapper extends BaseMapper<FeatureFlagEntity> {
|
||||||
|
}
|
||||||
@ -0,0 +1,48 @@
|
|||||||
|
-- mate_feature_flag: runtime-toggleable feature flag store.
|
||||||
|
--
|
||||||
|
-- Each row defines one named flag with optional KB / user whitelists and
|
||||||
|
-- a percentage rollout. Reads go through an in-memory cache that refreshes
|
||||||
|
-- on a 30-second timer (and immediately on admin write); the cache is
|
||||||
|
-- per-instance so multi-instance deployments converge within one tick.
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS mate_feature_flag (
|
||||||
|
id BIGINT AUTO_INCREMENT PRIMARY KEY,
|
||||||
|
flag_key VARCHAR(128) NOT NULL UNIQUE,
|
||||||
|
|
||||||
|
-- Master switch. When false, isEnabled() returns false regardless of whitelists.
|
||||||
|
enabled BOOLEAN NOT NULL DEFAULT FALSE,
|
||||||
|
|
||||||
|
-- Free-form note shown in the admin UI; not consumed by code paths.
|
||||||
|
description VARCHAR(512),
|
||||||
|
|
||||||
|
-- Comma-separated KB ids; NULL/empty means the flag applies to all KBs.
|
||||||
|
whitelist_kb_ids CLOB,
|
||||||
|
|
||||||
|
-- Comma-separated user ids; NULL/empty means the flag applies to all users.
|
||||||
|
whitelist_user_ids CLOB,
|
||||||
|
|
||||||
|
-- Hash-based gradual rollout (0..100). Only consulted when both whitelists
|
||||||
|
-- are empty; otherwise whitelist matching wins.
|
||||||
|
rollout_percent INT DEFAULT 0,
|
||||||
|
|
||||||
|
create_time TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
update_time TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
deleted INT NOT NULL DEFAULT 0
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_mff_key_enabled
|
||||||
|
ON mate_feature_flag (flag_key, enabled, deleted);
|
||||||
|
|
||||||
|
-- Seed wiki feature flags with safe defaults.
|
||||||
|
-- New capabilities ship disabled and are turned on via admin API or via
|
||||||
|
-- editing whitelist_kb_ids during gradual rollout.
|
||||||
|
MERGE INTO mate_feature_flag (flag_key, enabled, description) KEY (flag_key) VALUES
|
||||||
|
('wiki.ocr.enabled', FALSE, 'Image OCR / vision-in pipeline for wiki uploads'),
|
||||||
|
('wiki.compile.4stage.enabled', FALSE, 'Four-stage knowledge base compilation pipeline'),
|
||||||
|
('wiki.compile.cache.enabled', FALSE, 'Prompt cache layer for the wiki compile pipeline'),
|
||||||
|
('wiki.confidence.enabled', FALSE, 'Confidence taxonomy on wiki relations and pages'),
|
||||||
|
('wiki.hot_cache.enabled', FALSE, 'KB-level recent-activity snapshot injected into agent system prompt'),
|
||||||
|
('wiki.graph.insights.enabled', FALSE, 'Wiki graph insights panel (surprising connections, gaps, bridges)'),
|
||||||
|
('wiki.graph.adamic_adar.enabled', FALSE, 'Adamic-Adar graph signal (additive to existing four signals)'),
|
||||||
|
('wiki.graph.boundary.enabled', FALSE, 'Boundary score for surfacing dangling pages'),
|
||||||
|
('wiki.relation.cache.enabled', TRUE, 'Persistent cache for wiki page-to-page relation computation');
|
||||||
@ -0,0 +1,38 @@
|
|||||||
|
-- mate_feature_flag: runtime-toggleable feature flag store.
|
||||||
|
--
|
||||||
|
-- Each row defines one named flag with optional KB / user whitelists and
|
||||||
|
-- a percentage rollout. Reads go through an in-memory cache that refreshes
|
||||||
|
-- on a 30-second timer (and immediately on admin write); the cache is
|
||||||
|
-- per-instance so multi-instance deployments converge within one tick.
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS mate_feature_flag (
|
||||||
|
id BIGINT AUTO_INCREMENT PRIMARY KEY,
|
||||||
|
flag_key VARCHAR(128) NOT NULL,
|
||||||
|
enabled BOOLEAN NOT NULL DEFAULT FALSE,
|
||||||
|
description VARCHAR(512),
|
||||||
|
whitelist_kb_ids TEXT,
|
||||||
|
whitelist_user_ids TEXT,
|
||||||
|
rollout_percent INT DEFAULT 0,
|
||||||
|
|
||||||
|
create_time DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
|
||||||
|
update_time DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3),
|
||||||
|
deleted TINYINT NOT NULL DEFAULT 0,
|
||||||
|
|
||||||
|
UNIQUE KEY uk_mff_key (flag_key),
|
||||||
|
KEY idx_mff_key_flag (flag_key, enabled, deleted)
|
||||||
|
) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COLLATE = utf8mb4_unicode_ci
|
||||||
|
COMMENT = 'Runtime-toggleable feature flag store with KB/user whitelists and percentage rollout.';
|
||||||
|
|
||||||
|
-- Seed wiki feature flags with safe defaults. ON DUPLICATE KEY UPDATE
|
||||||
|
-- preserves existing operator overrides on re-run.
|
||||||
|
INSERT INTO mate_feature_flag (flag_key, enabled, description) VALUES
|
||||||
|
('wiki.ocr.enabled', FALSE, 'Image OCR / vision-in pipeline for wiki uploads'),
|
||||||
|
('wiki.compile.4stage.enabled', FALSE, 'Four-stage knowledge base compilation pipeline'),
|
||||||
|
('wiki.compile.cache.enabled', FALSE, 'Prompt cache layer for the wiki compile pipeline'),
|
||||||
|
('wiki.confidence.enabled', FALSE, 'Confidence taxonomy on wiki relations and pages'),
|
||||||
|
('wiki.hot_cache.enabled', FALSE, 'KB-level recent-activity snapshot injected into agent system prompt'),
|
||||||
|
('wiki.graph.insights.enabled', FALSE, 'Wiki graph insights panel (surprising connections, gaps, bridges)'),
|
||||||
|
('wiki.graph.adamic_adar.enabled', FALSE, 'Adamic-Adar graph signal (additive to existing four signals)'),
|
||||||
|
('wiki.graph.boundary.enabled', FALSE, 'Boundary score for surfacing dangling pages'),
|
||||||
|
('wiki.relation.cache.enabled', TRUE, 'Persistent cache for wiki page-to-page relation computation')
|
||||||
|
ON DUPLICATE KEY UPDATE description = VALUES(description);
|
||||||
Loading…
Reference in New Issue
Block a user